mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Added some simple time travel mechanism for part view.
In the moment it is not possible to show elements that were deleted.
This commit is contained in:
parent
a9fd1f9c68
commit
464a487a17
15 changed files with 450 additions and 27 deletions
|
@ -244,6 +244,16 @@ showing the sidebar (on devices with md or higher)
|
|||
* Bootstrap extensions
|
||||
*****************************************/
|
||||
|
||||
.bg-primary-striped {
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
var(--primary),
|
||||
var(--primary) 10px,
|
||||
var(--info) 10px,
|
||||
var(--info) 20px
|
||||
)
|
||||
}
|
||||
|
||||
.form-group-sm {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ use App\Form\Part\PartBaseType;
|
|||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use App\Services\Attachments\PartPreviewGenerator;
|
||||
use App\Services\LogSystem\TimeTravel;
|
||||
use App\Services\PricedetailHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -46,27 +47,48 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||
*/
|
||||
class PartController extends AbstractController
|
||||
{
|
||||
protected $attachmentManager;
|
||||
protected $pricedetailHelper;
|
||||
protected $partPreviewGenerator;
|
||||
|
||||
public function __construct(AttachmentManager $attachmentManager, PricedetailHelper $pricedetailHelper, PartPreviewGenerator $partPreviewGenerator)
|
||||
{
|
||||
$this->attachmentManager = $attachmentManager;
|
||||
$this->pricedetailHelper = $pricedetailHelper;
|
||||
$this->partPreviewGenerator = $partPreviewGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/info", name="part_info")
|
||||
* @Route("/{id}/info/{timestamp}", name="part_info")
|
||||
* @Route("/{id}", requirements={"id"="\d+"})
|
||||
*
|
||||
* @param Part $part
|
||||
* @param AttachmentManager $attachmentHelper
|
||||
* @param PricedetailHelper $pricedetailHelper
|
||||
* @param PartPreviewGenerator $previewGenerator
|
||||
* @return Response
|
||||
*/
|
||||
public function show(Part $part, AttachmentManager $attachmentHelper, PricedetailHelper $pricedetailHelper, PartPreviewGenerator $previewGenerator): Response
|
||||
public function show(Part $part, TimeTravel $timeTravel, ?string $timestamp = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', $part);
|
||||
|
||||
$timeTravel_timestamp = null;
|
||||
if ($timestamp !== null) {
|
||||
//If the timestamp only contains numbers interpret it as unix timestamp
|
||||
if (ctype_digit($timestamp)) {
|
||||
$timeTravel_timestamp = new \DateTime();
|
||||
$timeTravel_timestamp->setTimestamp((int) $timestamp);
|
||||
} else { //Try to parse it via DateTime
|
||||
$timeTravel_timestamp = new \DateTime($timestamp);
|
||||
}
|
||||
$timeTravel->revertEntityToTimestamp($part, $timeTravel_timestamp);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'Parts/info/show_part_info.html.twig',
|
||||
[
|
||||
'part' => $part,
|
||||
'attachment_helper' => $attachmentHelper,
|
||||
'pricedetail_helper' => $pricedetailHelper,
|
||||
'pictures' => $previewGenerator->getPreviewAttachments($part),
|
||||
'attachment_helper' => $this->attachmentManager,
|
||||
'pricedetail_helper' => $this->pricedetailHelper,
|
||||
'pictures' => $this->partPreviewGenerator->getPreviewAttachments($part),
|
||||
'timeTravel' => $timeTravel_timestamp
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -83,7 +105,7 @@ class PartController extends AbstractController
|
|||
* @return Response
|
||||
*/
|
||||
public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
|
||||
AttachmentManager $attachmentHelper, AttachmentSubmitHandler $attachmentSubmitHandler): Response
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('edit', $part);
|
||||
|
||||
|
@ -123,7 +145,7 @@ class PartController extends AbstractController
|
|||
[
|
||||
'part' => $part,
|
||||
'form' => $form->createView(),
|
||||
'attachment_helper' => $attachmentHelper,
|
||||
'attachment_helper' => $this->attachmentManager,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
* "user" = "App\Entity\User"
|
||||
* })
|
||||
*/
|
||||
abstract class AbstractDBElement
|
||||
abstract class AbstractDBElement implements \JsonSerializable
|
||||
{
|
||||
/** @var int|null The Identification number for this part. This value is unique for the element in this table.
|
||||
* Null if the element is not saved to DB yet.
|
||||
|
@ -93,4 +93,9 @@ abstract class AbstractDBElement
|
|||
* @return string The ID as a string;
|
||||
*/
|
||||
abstract public function getIDString(): string;
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return ['@id' => $this->getID()];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
|||
namespace App\Entity\Base;
|
||||
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
@ -34,7 +35,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @ORM\MappedSuperclass(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface
|
||||
abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface, TimeStampableInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
|
44
src/Entity/Contracts/TimeStampableInterface.php
Normal file
44
src/Entity/Contracts/TimeStampableInterface.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
namespace App\Entity\Contracts;
|
||||
|
||||
|
||||
use DateTime;
|
||||
|
||||
interface TimeStampableInterface
|
||||
{
|
||||
/**
|
||||
* Returns the last time when the element was modified.
|
||||
* Returns null if the element was not yet saved to DB yet.
|
||||
*
|
||||
* @return DateTime|null the time of the last edit
|
||||
*/
|
||||
public function getLastModified(): ?DateTime;
|
||||
|
||||
/**
|
||||
* Returns the date/time when the element was created.
|
||||
* Returns null if the element was not yet saved to DB yet.
|
||||
*
|
||||
* @return DateTime|null the creation time of the part
|
||||
*/
|
||||
public function getAddedDate(): ?DateTime;
|
||||
}
|
41
src/Entity/Contracts/TimeTravelInterface.php
Normal file
41
src/Entity/Contracts/TimeTravelInterface.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
namespace App\Entity\Contracts;
|
||||
|
||||
|
||||
interface TimeTravelInterface
|
||||
{
|
||||
/**
|
||||
* Checks if this entry has informations which data has changed.
|
||||
* @return bool True if this entry has informations about the changed data.
|
||||
*/
|
||||
public function hasOldDataInformations(): bool;
|
||||
|
||||
/** Returns the data the entity had before this log entry. */
|
||||
public function getOldData(): array;
|
||||
|
||||
/**
|
||||
* Returns the the timestamp associated with this change.
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getTimestamp(): \DateTime;
|
||||
}
|
|
@ -26,12 +26,13 @@ namespace App\Entity\LogSystem;
|
|||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\Contracts\TimeTravelInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class ElementDeletedLogEntry extends AbstractLogEntry
|
||||
class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInterface
|
||||
{
|
||||
protected $typeString = 'element_deleted';
|
||||
|
||||
|
@ -65,4 +66,31 @@ class ElementDeletedLogEntry extends AbstractLogEntry
|
|||
{
|
||||
return $this->extra['n'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the old data for this entry.
|
||||
* @param array $old_data
|
||||
* @return $this
|
||||
*/
|
||||
public function setOldData(array $old_data): self
|
||||
{
|
||||
$this->extra['o'] = $old_data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasOldDataInformations(): bool
|
||||
{
|
||||
return !empty($this->extra['d']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOldData(): array
|
||||
{
|
||||
return $this->extra['d'] ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,13 @@ declare(strict_types=1);
|
|||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Contracts\TimeTravelInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class ElementEditedLogEntry extends AbstractLogEntry
|
||||
class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface
|
||||
{
|
||||
protected $typeString = 'element_edited';
|
||||
|
||||
|
@ -42,6 +43,17 @@ class ElementEditedLogEntry extends AbstractLogEntry
|
|||
$this->setTargetElement($changed_element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the old data for this entry.
|
||||
* @param array $old_data
|
||||
* @return $this
|
||||
*/
|
||||
public function setOldData(array $old_data): self
|
||||
{
|
||||
$this->extra['d'] = $old_data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message associated with this edit change.
|
||||
*
|
||||
|
@ -51,4 +63,20 @@ class ElementEditedLogEntry extends AbstractLogEntry
|
|||
{
|
||||
return $this->extra['m'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasOldDataInformations(): bool
|
||||
{
|
||||
return !empty($this->extra['d']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOldData(): array
|
||||
{
|
||||
return $this->extra['d'] ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace App\Entity\Parts;
|
|||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use App\Validator\Constraints\ValidPartLot;
|
||||
use DateTime;
|
||||
|
@ -42,7 +43,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @ORM\HasLifecycleCallbacks()
|
||||
* @ValidPartLot()
|
||||
*/
|
||||
class PartLot extends AbstractDBElement
|
||||
class PartLot extends AbstractDBElement implements TimeStampableInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace App\Entity\PriceInformations;
|
|||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
@ -67,7 +68,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @ORM\Entity()
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class Orderdetail extends AbstractDBElement
|
||||
class Orderdetail extends AbstractDBElement implements TimeStampableInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ namespace App\Entity\PriceInformations;
|
|||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
@ -66,7 +67,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @ORM\HasLifecycleCallbacks()
|
||||
* @UniqueEntity(fields={"orderdetail", "min_discount_quantity"})
|
||||
*/
|
||||
class Pricedetail extends AbstractDBElement
|
||||
class Pricedetail extends AbstractDBElement implements TimeStampableInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
|
|
@ -31,15 +31,19 @@ use Doctrine\Common\EventSubscriber;
|
|||
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use Doctrine\ORM\Event\PostFlushEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class EventLoggerSubscriber implements EventSubscriber
|
||||
{
|
||||
protected $logger;
|
||||
protected $serializer;
|
||||
|
||||
public function __construct(EventLogger $logger)
|
||||
public function __construct(EventLogger $logger, SerializerInterface $serializer)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $eventArgs)
|
||||
|
@ -55,6 +59,7 @@ class EventLoggerSubscriber implements EventSubscriber
|
|||
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||
if ($this->validEntity($entity)) {
|
||||
$log = new ElementEditedLogEntry($entity);
|
||||
$this->saveChangeSet($entity, $log, $uow);
|
||||
$this->logger->log($log);
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +67,7 @@ class EventLoggerSubscriber implements EventSubscriber
|
|||
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||
if ($this->validEntity($entity)) {
|
||||
$log = new ElementDeletedLogEntry($entity);
|
||||
$this->saveChangeSet($entity, $log, $uow);
|
||||
$this->logger->log($log);
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +76,18 @@ class EventLoggerSubscriber implements EventSubscriber
|
|||
$uow->computeChangeSets();
|
||||
}
|
||||
|
||||
protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, UnitOfWork $uow): void
|
||||
{
|
||||
if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) {
|
||||
throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!');
|
||||
}
|
||||
|
||||
$changeSet = $uow->getEntityChangeSet($entity);
|
||||
dump($changeSet);
|
||||
$old_data = array_diff(array_combine(array_keys($changeSet), array_column($changeSet, 0)), [null]);
|
||||
$logEntry->setOldData($old_data);
|
||||
}
|
||||
|
||||
public function postPersist(LifecycleEventArgs $args)
|
||||
{
|
||||
//Create an log entry
|
||||
|
|
|
@ -27,6 +27,7 @@ namespace App\Repository;
|
|||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\LogSystem\AbstractLogEntry;
|
||||
use App\Entity\LogSystem\ElementCreatedLogEntry;
|
||||
use App\Entity\LogSystem\ElementDeletedLogEntry;
|
||||
use App\Entity\LogSystem\ElementEditedLogEntry;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
@ -63,6 +64,48 @@ class LogEntryRepository extends EntityRepository
|
|||
return $this->findBy(['element' => $element], ['timestamp' => $order], $limit, $offset);
|
||||
}
|
||||
|
||||
public function getTimetravelDataForElement(AbstractDBElement $element, \DateTime $until): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('log');
|
||||
$qb->select('log')
|
||||
//->where('log INSTANCE OF App\Entity\LogSystem\ElementEditedLogEntry')
|
||||
->where('log INSTANCE OF ' . ElementEditedLogEntry::class)
|
||||
->andWhere('log.target_type = :target_type')
|
||||
->andWhere('log.target_id = :target_id')
|
||||
->andWhere('log.timestamp >= :until')
|
||||
->orderBy('log.timestamp', 'DESC');
|
||||
|
||||
$qb->setParameters([
|
||||
'target_type' => AbstractLogEntry::targetTypeClassToID(get_class($element)),
|
||||
'target_id' => $element->getID(),
|
||||
'until' => $until
|
||||
]);
|
||||
|
||||
$query = $qb->getQuery();
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
public function getElementExistedAtTimestamp(AbstractDBElement $element, \DateTime $timestamp): bool
|
||||
{
|
||||
$qb = $this->createQueryBuilder('log');
|
||||
$qb->select('count(log)')
|
||||
->where('log INSTANCE OF ' . ElementCreatedLogEntry::class)
|
||||
->andWhere('log.target_type = :target_type')
|
||||
->andWhere('log.target_id = :target_id')
|
||||
->andWhere('log.timestamp >= :until')
|
||||
->orderBy('log.timestamp', 'DESC');
|
||||
|
||||
$qb->setParameters([
|
||||
'target_type' => AbstractLogEntry::targetTypeClassToID(get_class($element)),
|
||||
'target_id' => $element->getID(),
|
||||
'until' => $timestamp
|
||||
]);
|
||||
|
||||
$query = $qb->getQuery();
|
||||
$count = $query->getSingleScalarResult();
|
||||
return !($count > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last log entries ordered by timestamp.
|
||||
*
|
||||
|
|
168
src/Services/LogSystem/TimeTravel.php
Normal file
168
src/Services/LogSystem/TimeTravel.php
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
namespace App\Services\LogSystem;
|
||||
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Contracts\TimeStampableInterface;
|
||||
use App\Entity\Contracts\TimeTravelInterface;
|
||||
use App\Entity\LogSystem\AbstractLogEntry;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Proxies\__CG__\App\Entity\Attachments\AttachmentType;
|
||||
|
||||
class TimeTravel
|
||||
{
|
||||
protected $em;
|
||||
protected $repo;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->repo = $em->getRepository(AbstractLogEntry::class);
|
||||
}
|
||||
|
||||
public function revertEntityToTimestamp(AbstractDBElement $element, \DateTime $timestamp, array $reverted_elements = [])
|
||||
{
|
||||
if (!$element instanceof TimeStampableInterface) {
|
||||
throw new \InvalidArgumentException('$element must have a Timestamp!');
|
||||
}
|
||||
|
||||
if ($timestamp > new \DateTime('now')) {
|
||||
throw new \InvalidArgumentException('You can not travel to the future (yet)...');
|
||||
}
|
||||
|
||||
//Skip this process if already were reverted...
|
||||
if (in_array($element, $reverted_elements)) {
|
||||
return;
|
||||
}
|
||||
$reverted_elements[] = $element;
|
||||
|
||||
$history = $this->repo->getTimetravelDataForElement($element, $timestamp);
|
||||
|
||||
/*
|
||||
if (!$this->repo->getElementExistedAtTimestamp($element, $timestamp)) {
|
||||
$element = null;
|
||||
return;
|
||||
}*/
|
||||
|
||||
foreach ($history as $logEntry) {
|
||||
$this->applyEntry($element, $logEntry);
|
||||
}
|
||||
|
||||
// Revert any of the associated elements
|
||||
$metadata = $this->em->getClassMetadata(get_class($element));
|
||||
$associations = $metadata->getAssociationMappings();
|
||||
foreach ($associations as $field => $mapping) {
|
||||
if (
|
||||
($element instanceof AbstractStructuralDBElement && ($field === 'parts' || $field === 'children'))
|
||||
|| ($element instanceof AttachmentType && $field === 'attachments')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//Revert many to one association
|
||||
if (
|
||||
$mapping['type'] === ClassMetadata::MANY_TO_ONE
|
||||
|| $mapping['type'] === ClassMetadata::ONE_TO_ONE
|
||||
) {
|
||||
$target_element = $this->getField($element, $field);
|
||||
if ($target_element !== null && $element->getLastModified() > $timestamp) {
|
||||
$this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements);
|
||||
}
|
||||
} elseif (
|
||||
($mapping['type'] === ClassMetadata::MANY_TO_MANY
|
||||
|| $mapping['type'] === ClassMetadata::ONE_TO_MANY)
|
||||
&& $mapping['isOwningSide'] === false
|
||||
) {
|
||||
$target_elements = $this->getField($element, $field);
|
||||
if ($target_elements === null || count($target_elements) > 10) {
|
||||
continue;
|
||||
}
|
||||
foreach ($target_elements as $target_element) {
|
||||
if ($target_element !== null && $element->getLastModified() > $timestamp) {
|
||||
//Remove the element from collection, if it did not existed at $timestamp
|
||||
if (!$this->repo->getElementExistedAtTimestamp($target_element, $timestamp)) {
|
||||
if ($target_elements instanceof Collection) {
|
||||
$target_elements->removeElement($target_element);
|
||||
}
|
||||
}
|
||||
$this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logEntry): void
|
||||
{
|
||||
//Skip if this does not provide any info...
|
||||
if (!$logEntry->hasOldDataInformations()) {
|
||||
return;
|
||||
}
|
||||
if (!$element instanceof TimeStampableInterface) {
|
||||
return;
|
||||
}
|
||||
$metadata = $this->em->getClassMetadata(get_class($element));
|
||||
$old_data = $logEntry->getOldData();
|
||||
|
||||
foreach ($old_data as $field => $data) {
|
||||
if ($metadata->hasField($field)) {
|
||||
$this->setField($element, $field, $data);
|
||||
}
|
||||
if ($metadata->hasAssociation($field)) {
|
||||
$target_class = $metadata->getAssociationMapping($field)['targetEntity'];
|
||||
$target_id = null;
|
||||
//Try to extract the old ID:
|
||||
if (is_array($data) && isset($data['@id'])) {
|
||||
$target_id = $data['@id'];
|
||||
} else {
|
||||
throw new \RuntimeException('The given $logEntry contains invalid informations!');
|
||||
}
|
||||
$entity = $this->em->getPartialReference($target_class, $target_id);
|
||||
$this->setField($element, $field, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setField($element, 'lastModified', $logEntry->getTimestamp());
|
||||
}
|
||||
|
||||
protected function getField(AbstractDBElement $element, string $field)
|
||||
{
|
||||
$reflection = new \ReflectionClass(get_class($element));
|
||||
$property = $reflection->getProperty($field);
|
||||
$property->setAccessible(true);
|
||||
return $property->getValue($element);
|
||||
}
|
||||
|
||||
protected function setField(AbstractDBElement $element, string $field, $new_value)
|
||||
{
|
||||
$reflection = new \ReflectionClass(get_class($element));
|
||||
$property = $reflection->getProperty($field);
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($element, $new_value);
|
||||
}
|
||||
}
|
|
@ -4,16 +4,28 @@
|
|||
{% trans %}part.info.title{% endtrans %} {{ part.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_type %}
|
||||
|
||||
{% if timeTravel == null %}
|
||||
bg-primary text-white
|
||||
{% else %}
|
||||
bg-primary-striped text-white
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fa {{ part.favorite ? 'fa-star' : 'fa-info-circle'}} fa-fw" aria-hidden="true"></i>
|
||||
{% trans %}part.info.title{% endtrans %} <b>"{{ part.name }}"</b>
|
||||
{% if timeTravel != null %}
|
||||
<i>({{ timeTravel | format_datetime('short') }})</i>
|
||||
{% endif %}
|
||||
<div class="float-right">
|
||||
{% trans %}id.label{% endtrans %}: {{ part.id }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include "Parts/info/_main_infos.html.twig" %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue