diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 1265bf32..f09e20b8 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -96,16 +96,6 @@ class PartController extends AbstractController { $this->denyAccessUnlessGranted('read', $part); - $table = $dataTable->createFromType(LogDataTable::class, [ - 'filter_elements' => $historyHelper->getAssociatedElements($part), - 'mode' => 'element_history' - ], ['pageLength' => 10]) - ->handleRequest($request); - - if ($table->isCallback()) { - return $table->getResponse(); - } - $timeTravel_timestamp = null; if ($timestamp !== null) { //If the timestamp only contains numbers interpret it as unix timestamp @@ -118,6 +108,16 @@ class PartController extends AbstractController $timeTravel->revertEntityToTimestamp($part, $timeTravel_timestamp); } + $table = $dataTable->createFromType(LogDataTable::class, [ + 'filter_elements' => $historyHelper->getAssociatedElements($part), + 'mode' => 'element_history' + ], ['pageLength' => 10]) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + return $this->render( 'Parts/info/show_part_info.html.twig', [ diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 2688a8d7..e894355c 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -49,6 +49,7 @@ use App\DataTables\Column\LogEntryTargetColumn; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; @@ -211,8 +212,9 @@ class LogDataTable implements DataTableTypeInterface 'icon' => 'fas fa-fw fa-eye', 'href' => function ($value, AbstractLogEntry $context) { if ( - $context instanceof TimeTravelInterface - && $context->hasOldDataInformations() + ($context instanceof TimeTravelInterface + && $context->hasOldDataInformations()) + || $context instanceof CollectionElementDeleted ) { try { $target = $this->logRepo->getTargetElement($context); diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index 80b4034a..9d211e2b 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -81,7 +81,8 @@ use Psr\Log\LogLevel; * 7 = "ElementEditedLogEntry", * 8 = "ConfigChangedLogEntry", * 9 = "InstockChangedLogEntry", - * 10 = "DatabaseUpdatedLogEntry" + * 10 = "DatabaseUpdatedLogEntry", + * 11 = "CollectionElementDeleted" * }) */ abstract class AbstractLogEntry extends AbstractDBElement diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php new file mode 100644 index 00000000..5b7155e1 --- /dev/null +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -0,0 +1,88 @@ +. + */ + +namespace App\Entity\LogSystem; + + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Contracts\NamedElementInterface; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity() + * This log entry is created when an element is deleted, that is used in a collection of an other entity. + * This is needed to signal time travel, that it has to undelete the deleted entity. + */ +class CollectionElementDeleted extends AbstractLogEntry +{ + protected $typeString = 'collection_element_deleted'; + protected $level = self::LEVEL_INFO; + + public function __construct(AbstractDBElement $changed_element, string $collection_name, AbstractDBElement $deletedElement) + { + parent::__construct(); + + $this->level = self::LEVEL_INFO; + $this->setTargetElement($changed_element); + $this->extra['n'] = $collection_name; + $this->extra['c'] = self::targetTypeClassToID(get_class($deletedElement)); + $this->extra['i'] = $deletedElement->getID(); + if ($deletedElement instanceof NamedElementInterface) { + $this->extra['o'] = $deletedElement->getName(); + } + } + + /** + * Get the name of the collection (on target element) that was changed. + * @return string + */ + public function getCollectionName(): string + { + return $this->extra['n']; + } + + /** + * Gets the name of the element that was deleted. + * Return null, if the element did not have a name. + * @return string|null + */ + public function getOldName(): ?string + { + return $this->extra['o'] ?? null; + } + + /** + * Returns the class of the deleted element. + * @return string + */ + public function getDeletedElementClass(): string + { + return self::targetTypeIdToClass($this->extra['c']); + } + + /** + * Returns the ID of the deleted element. + * @return int + */ + public function getDeletedElementID(): int + { + return $this->extra['i']; + } +} \ No newline at end of file diff --git a/src/Entity/LogSystem/ElementDeletedLogEntry.php b/src/Entity/LogSystem/ElementDeletedLogEntry.php index 93912784..50c5628b 100644 --- a/src/Entity/LogSystem/ElementDeletedLogEntry.php +++ b/src/Entity/LogSystem/ElementDeletedLogEntry.php @@ -102,7 +102,7 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter */ public function hasOldDataInformations(): bool { - return !empty($this->extra['d']); + return !empty($this->extra['o']); } /** @@ -110,7 +110,7 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter */ public function getOldData(): array { - return $this->extra['d'] ?? []; + return $this->extra['o'] ?? []; } /** diff --git a/src/EventSubscriber/EventLoggerSubscriber.php b/src/EventSubscriber/EventLoggerSubscriber.php index 3e0b9d36..10d7f73c 100644 --- a/src/EventSubscriber/EventLoggerSubscriber.php +++ b/src/EventSubscriber/EventLoggerSubscriber.php @@ -20,24 +20,45 @@ namespace App\EventSubscriber; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Entity\Parts\PartLot; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; use App\Entity\UserSystem\User; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\EventLogger; use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\EntityManagerInterface; 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\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\SerializerInterface; class EventLoggerSubscriber implements EventSubscriber { + /** @var array The given fields will not be saved, because they contain sensitive informations */ + protected const FIELD_BLACKLIST = [ + User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'], + ]; + + /** @var array If elements of the given class are deleted, a log for the given fields will be triggered */ + protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [ + PartLot::class => ['part'], + Orderdetail::class => ['part'], + Pricedetail::class => ['orderdetail'], + Attachment::class => ['element'], + ]; protected const MAX_STRING_LENGTH = 2000; @@ -47,13 +68,15 @@ class EventLoggerSubscriber implements EventSubscriber protected $save_changed_fields; protected $save_changed_data; protected $save_removed_data; + protected $propertyAccessor; public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper, - bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data) + bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor) { $this->logger = $logger; $this->serializer = $serializer; $this->eventCommentHelper = $commentHelper; + $this->propertyAccessor = $propertyAccessor; $this->save_changed_fields = $save_changed_fields; $this->save_changed_data = $save_changed_data; @@ -72,68 +95,22 @@ class EventLoggerSubscriber implements EventSubscriber foreach ($uow->getScheduledEntityUpdates() as $entity) { if ($this->validEntity($entity)) { - $log = new ElementEditedLogEntry($entity); - if ($this->save_changed_data) { - $this->saveChangeSet($entity, $log, $uow); - } elseif ($this->save_changed_fields) { - $changed_fields = array_keys($uow->getEntityChangeSet($entity)); - $log->setChangedFields($changed_fields); - } - //Add user comment to log entry - if ($this->eventCommentHelper->isMessageSet()) { - $log->setComment($this->eventCommentHelper->getMessage()); - } - $this->logger->log($log); + $this->logElementEdited($entity, $em); } } foreach ($uow->getScheduledEntityDeletions() as $entity) { if ($this->validEntity($entity)) { - $log = new ElementDeletedLogEntry($entity); - //Add user comment to log entry - if ($this->eventCommentHelper->isMessageSet()) { - $log->setComment($this->eventCommentHelper->getMessage()); - } - if ($this->save_removed_data) { - $this->saveChangeSet($entity, $log, $uow); - } - $this->logger->log($log); + $this->logElementDeleted($entity, $em); } } - $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); - $old_data = array_diff(array_combine(array_keys($changeSet), array_column($changeSet, 0)), [null]); - //Restrict length of string fields, to save memory... - $old_data = array_map(function ($value) { - if (is_string($value)) { - return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...'); - } - - return $value; - }, $old_data); - - //Dont save sensitive fields to log - if ($entity instanceof User) { - unset($old_data['password'], $old_data['pw_reset_token'], $old_data['backupCodes']); - } - - - $logEntry->setOldData($old_data); - } - public function postPersist(LifecycleEventArgs $args) { - //Create an log entry + //Create an log entry, we have to do this post persist, cause we have to know the ID /** @var AbstractDBElement $entity */ $entity = $args->getObject(); @@ -160,6 +137,140 @@ class EventLoggerSubscriber implements EventSubscriber $this->eventCommentHelper->clearMessage(); } + protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void + { + $log = new ElementDeletedLogEntry($entity); + //Add user comment to log entry + if ($this->eventCommentHelper->isMessageSet()) { + $log->setComment($this->eventCommentHelper->getMessage()); + } + if ($this->save_removed_data) { + //The 4th param is important here, as we delete the element... + $this->saveChangeSet($entity, $log, $em, true); + } + $this->logger->log($log); + + //Check if we have to log CollectionElementDeleted entries + if ($this->save_changed_data) { + $metadata = $em->getClassMetadata(get_class($entity)); + $mappings = $metadata->getAssociationMappings(); + //Check if class is whitelisted for CollectionElementDeleted entry + foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) { + if (is_a($entity, $class)) { + //Check names + foreach ($mappings as $field => $mapping) { + if (in_array($field, $whitelist)) { + $changed = $this->propertyAccessor->getValue($entity, $field); + $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity); + $this->logger->log($log); + } + } + } + } + } + } + + protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void + { + $uow = $em->getUnitOfWork(); + + $log = new ElementEditedLogEntry($entity); + if ($this->save_changed_data) { + $this->saveChangeSet($entity, $log, $em); + } elseif ($this->save_changed_fields) { + $changed_fields = array_keys($uow->getEntityChangeSet($entity)); + $log->setChangedFields($changed_fields); + } + //Add user comment to log entry + if ($this->eventCommentHelper->isMessageSet()) { + $log->setComment($this->eventCommentHelper->getMessage()); + } + $this->logger->log($log); + } + + /** + * Check if the given element class has restrictions to its fields + * @param AbstractDBElement $element + * @return bool True if there are restrictions, and further checking is needed + */ + public function hasFieldRestrictions(AbstractDBElement $element): bool + { + foreach (static::FIELD_BLACKLIST as $class => $blacklist) { + if (is_a($element, $class)) { + return true; + } + } + + return false; + } + + /** + * Filter out every forbidden field and return the cleaned array. + * @param AbstractDBElement $element + * @param array $fields + * @return array + */ + protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array + { + if (!$this->hasFieldRestrictions($element)) { + return $fields; + } + + return array_filter($fields, function ($value, $key) use ($element) { + //Associative array (save changed data) case + if (is_string($key)) { + return $this->shouldFieldBeSaved($element, $key); + } + + return $this->shouldFieldBeSaved($element, $value); + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * Checks if the field of the given element should be saved (if it is not blacklisted). + * @param AbstractDBElement $element + * @param string $field_name + * @return bool + */ + public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool + { + foreach (static::FIELD_BLACKLIST as $class => $blacklist) { + if (is_a($element, $class) && in_array($field_name, $blacklist)) { + return false; + } + } + + //By default allow every field. + return true; + } + + protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, $element_deleted = false): void + { + $uow = $em->getUnitOfWork(); + + if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) { + throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!'); + } + + if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content + $old_data = $uow->getOriginalEntityData($entity); + } else { //Otherwise we have to get it from entity changeset + $changeSet = $uow->getEntityChangeSet($entity); + $old_data = array_diff(array_combine(array_keys($changeSet), array_column($changeSet, 0)), [null]); + } + $this->filterFieldRestrictions($entity, $old_data); + + //Restrict length of string fields, to save memory... + $old_data = array_map(function ($value) { + if (is_string($value)) { + return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...'); + } + + return $value; + }, $old_data); + + $logEntry->setOldData($old_data); + } /** * Check if the given entity can be logged. * @param object $entity diff --git a/src/Repository/LogEntryRepository.php b/src/Repository/LogEntryRepository.php index 4c31e1b8..71d4a5f5 100644 --- a/src/Repository/LogEntryRepository.php +++ b/src/Repository/LogEntryRepository.php @@ -44,6 +44,7 @@ namespace App\Repository; use App\Entity\Base\AbstractDBElement; use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; @@ -82,12 +83,51 @@ class LogEntryRepository extends EntityRepository return $this->findBy(['element' => $element], ['timestamp' => $order], $limit, $offset); } + /** + * Try to get a log entry that contains the information to undete a given element + * @param string $class The class of the element that should be undeleted + * @param int $id The ID of the element that should be deleted + * @return ElementDeletedLogEntry + */ + public function getUndeleteDataForElement(string $class, int $id): ElementDeletedLogEntry + { + $qb = $this->createQueryBuilder('log'); + $qb->select('log') + //->where('log INSTANCE OF App\Entity\LogSystem\ElementEditedLogEntry') + ->where('log INSTANCE OF ' . ElementDeletedLogEntry::class) + ->andWhere('log.target_type = :target_type') + ->andWhere('log.target_id = :target_id') + ->orderBy('log.timestamp', 'DESC') + ->setMaxResults(1); + + $qb->setParameters([ + 'target_type' => AbstractLogEntry::targetTypeClassToID($class), + 'target_id' => $id, + ]); + + $query = $qb->getQuery(); + + $results = $query->execute(); + + if (empty($results)) { + throw new \RuntimeException("No undelete data could be found for this element"); + } + return $results[0]; + } + + /** + * Gets all log entries that are related to time travelling + * @param AbstractDBElement $element The element for which the time travel data should be retrieved + * @param \DateTime $until Back to which timestamp should the data be get (including the timestamp) + * @return AbstractLogEntry[] + */ 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) + ->orWhere('log INSTANCE OF ' . CollectionElementDeleted::class) ->andWhere('log.target_type = :target_type') ->andWhere('log.target_id = :target_id') ->andWhere('log.timestamp >= :until') @@ -103,6 +143,12 @@ class LogEntryRepository extends EntityRepository return $query->execute(); } + /** + * Check if the given element has existed at the given timestamp + * @param AbstractDBElement $element + * @param \DateTime $timestamp + * @return bool True if the element existed at the given timestamp + */ public function getElementExistedAtTimestamp(AbstractDBElement $element, \DateTime $timestamp): bool { $qb = $this->createQueryBuilder('log'); diff --git a/src/Services/LogSystem/LogEntryExtraFormatter.php b/src/Services/LogSystem/LogEntryExtraFormatter.php index 8aa56166..c4e83187 100644 --- a/src/Services/LogSystem/LogEntryExtraFormatter.php +++ b/src/Services/LogSystem/LogEntryExtraFormatter.php @@ -43,6 +43,7 @@ declare(strict_types=1); namespace App\Services\LogSystem; use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\DatabaseUpdatedLogEntry; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; @@ -52,6 +53,8 @@ use App\Entity\LogSystem\InstockChangedLogEntry; use App\Entity\LogSystem\UserLoginLogEntry; use App\Entity\LogSystem\UserLogoutLogEntry; use App\Entity\LogSystem\UserNotAllowedLogEntry; +use App\Services\ElementTypeNameGenerator; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -60,10 +63,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class LogEntryExtraFormatter { protected $translator; + protected $elementTypeNameGenerator; - public function __construct(TranslatorInterface $translator) + public function __construct(TranslatorInterface $translator, ElementTypeNameGenerator $elementTypeNameGenerator) { $this->translator = $translator; + $this->elementTypeNameGenerator = $elementTypeNameGenerator; } /** @@ -174,6 +179,15 @@ class LogEntryExtraFormatter ); } + if ($context instanceof CollectionElementDeleted) { + return sprintf('%s: %s: %s (%s)', + $this->translator->trans('log.collection_deleted.deleted'), + $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()), + $context->getOldName() ?? $context->getDeletedElementID(), + $context->getCollectionName() + ); + } + if ($context instanceof UserNotAllowedLogEntry) { return htmlspecialchars($context->getMessage()); } diff --git a/src/Services/LogSystem/TimeTravel.php b/src/Services/LogSystem/TimeTravel.php index 9b51d2e6..b8edb7ba 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -27,6 +27,8 @@ use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Contracts\TimeStampableInterface; use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; +use App\Entity\LogSystem\ElementEditedLogEntry; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; @@ -43,6 +45,31 @@ class TimeTravel $this->repo = $em->getRepository(AbstractLogEntry::class); } + /** + * Undeletes the element with the given ID. + * @param string $class The class name of the element that should be undeleted + * @param int $id The ID of the element that should be undeleted. + * @return AbstractDBElement + */ + public function undeleteEntity(string $class, int $id): AbstractDBElement + { + $log = $this->repo->getUndeleteDataForElement($class, $id); + $element = new $class(); + $this->applyEntry($element, $log); + + //Set internal ID so the element can be reverted + $this->setField($element, 'id', $id); + + return $element; + } + + /** + * Revert the given element to the state it has on the given timestamp + * @param AbstractDBElement $element + * @param \DateTime $timestamp + * @param AbstractLogEntry[] $reverted_elements + * @throws \Exception + */ public function revertEntityToTimestamp(AbstractDBElement $element, \DateTime $timestamp, array $reverted_elements = []) { if (!$element instanceof TimeStampableInterface) { @@ -68,7 +95,23 @@ class TimeTravel }*/ foreach ($history as $logEntry) { - $this->applyEntry($element, $logEntry); + if ($logEntry instanceof ElementEditedLogEntry) { + $this->applyEntry($element, $logEntry); + } + if ($logEntry instanceof CollectionElementDeleted) { + //Undelete element and add it to collection again + $undeleted = $this->undeleteEntity( + $logEntry->getDeletedElementClass(), + $logEntry->getDeletedElementID() + ); + if ($this->repo->getElementExistedAtTimestamp($undeleted, $timestamp)) { + $this->revertEntityToTimestamp($undeleted, $timestamp, $reverted_elements); + $collection = $this->getField($element, $logEntry->getCollectionName()); + if ($collection instanceof Collection) { + $collection->add($undeleted); + } + } + } } // Revert any of the associated elements @@ -83,7 +126,7 @@ class TimeTravel } - //Revert many to one association + //Revert many to one association (one element in property) if ( $mapping['type'] === ClassMetadata::MANY_TO_ONE || $mapping['type'] === ClassMetadata::ONE_TO_ONE @@ -92,7 +135,7 @@ class TimeTravel if ($target_element !== null && $element->getLastModified() > $timestamp) { $this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements); } - } elseif ( + } elseif ( //Revert *_TO_MANY associations (collection properties) ($mapping['type'] === ClassMetadata::MANY_TO_MANY || $mapping['type'] === ClassMetadata::ONE_TO_MANY) && $mapping['isOwningSide'] === false @@ -102,7 +145,7 @@ class TimeTravel continue; } foreach ($target_elements as $target_element) { - if ($target_element !== null && $element->getLastModified() > $timestamp) { + 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) { @@ -117,6 +160,12 @@ class TimeTravel } } + /** + * Apply the changeset in the given LogEntry to the element + * @param AbstractDBElement $element + * @param TimeTravelInterface $logEntry + * @throws \Doctrine\ORM\Mapping\MappingException + */ public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logEntry): void { //Skip if this does not provide any info... @@ -134,16 +183,13 @@ class TimeTravel $this->setField($element, $field, $data); } if ($metadata->hasAssociation($field)) { - $target_class = $metadata->getAssociationMapping($field)['targetEntity']; - $target_id = null; + $mapping = $metadata->getAssociationMapping($field); + $target_class = $mapping['targetEntity']; //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, $data['@id']); + $this->setField($element, $field, $entity); } - $entity = $this->em->getPartialReference($target_class, $target_id); - $this->setField($element, $field, $entity); } }