Created
August 2, 2022 07:31
-
-
Save ThomasLabstep/bb138d8f36a22265729f76d565ed317c to your computer and use it in GitHub Desktop.
Gedmo Loggable - Collection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
/** | |
* @author Thomas Bullier <thomas@labstep.com> | |
*/ | |
class LogCollection | |
{ | |
/** @var string Add action */ | |
public const ACTION_ADD = 'add'; | |
/** @var string Remove action */ | |
public const ACTION_REMOVE = 'remove'; | |
/** @var array All log collection types */ | |
public const ACTION_TYPES = [ | |
LogCollection::ACTION_ADD, | |
LogCollection::ACTION_REMOVE, | |
]; | |
/** | |
* @var int Primary key | |
* @ORM\Id | |
* @ORM\Column(type="integer") | |
* @ORM\GeneratedValue(strategy="AUTO") | |
*/ | |
protected $id; | |
/** | |
* @var \DateTime|null Creation date | |
* @Gedmo\Timestampable(on="create") | |
* @ORM\Column(type="datetime") | |
*/ | |
protected $loggedAt; | |
/** | |
* @var string Object class | |
* @ORM\Column(type="string", length=190) | |
* @Assert\NotBlank() | |
*/ | |
protected $parentClass; | |
/** | |
* @var int Object id | |
* @ORM\Column(type="integer") | |
* @Assert\NotBlank() | |
*/ | |
protected $parentId; | |
/** | |
* @var string Object class | |
* @ORM\Column(type="string", length=190) | |
* @Assert\NotBlank() | |
*/ | |
protected $childClass; | |
/** | |
* @var int Object id | |
* @ORM\Column(type="integer") | |
* @Assert\NotBlank() | |
*/ | |
protected $childId; | |
/** | |
* @var string Field changed | |
* @ORM\Column(type="string", length=190) | |
* @Assert\NotBlank() | |
*/ | |
protected $field; | |
/** | |
* @var string Type of change | |
* @ORM\Column(type="string", length=190) | |
* @Assert\Choice(LogCollection::ACTION_TYPES) | |
*/ | |
protected $action; | |
/** | |
* @var User User | |
* | |
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User") | |
*/ | |
protected $user; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
/** | |
* @author Thomas Bullier <thomas@labstep.com> | |
*/ | |
namespace AppBundle\Listener; | |
use AppBundle\Entity\LogCollection; | |
use AppBundle\Entity\LogEntry; | |
use AppBundle\Entity\User; | |
use AppBundle\Service\UserService; | |
use Doctrine\Common\Util\ClassUtils; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Doctrine\ORM\Event\LifecycleEventArgs; | |
use Doctrine\ORM\Event\OnFlushEventArgs; | |
use Doctrine\ORM\Event\PostFlushEventArgs; | |
use Doctrine\ORM\PersistentCollection; | |
use Doctrine\ORM\UnitOfWork; | |
/** | |
* Creates LogCollection when adding or removing child in collections. | |
*/ | |
class LogCollectionListener | |
{ | |
/** @var UserService UserService */ | |
protected $userService; | |
/** @var array Keeping track of new LogCollection */ | |
protected $newLogCollections; | |
/** @var array Keeping track of LogCollection to set childId/parentId */ | |
protected $logCollections; | |
/** @var array List of LogCollection parents to keep track. */ | |
protected $parents; | |
/** @var array List of LogCollection children to keep track. */ | |
protected $children; | |
/** | |
* Constructor. | |
* | |
* @param UserService $userService | |
*/ | |
public function __construct( | |
UserService $userService | |
) { | |
$this->userService = $userService; | |
$this->newLogCollections = []; | |
$this->logCollections = []; | |
$this->parents = []; | |
$this->children = []; | |
} | |
/** | |
* Stores a add LogCollection entry for each insert of supported classes entities. | |
* | |
* @param OnFlushEventArgs $args | |
*/ | |
public function onFlush(OnFlushEventArgs $args) | |
{ | |
$em = $args->getEntityManager(); | |
$unitOfWork = $em->getUnitOfWork(); | |
$user = $this->userService->getLoggedUser(); | |
if (!$user) { | |
return; | |
} | |
foreach ($unitOfWork->getScheduledCollectionUpdates() as $collection) { | |
$association = $collection->getMapping(); | |
$owner = $collection->getOwner(); | |
$this->onInsert($collection, $association, $owner, $user); | |
$this->onDelete($collection, $association, $owner, $user, $em, $unitOfWork); | |
} | |
} | |
/** | |
* When removing an item from a collection, store a logCollection action 'add' in memory. | |
* | |
* @param PersistentCollection $collection | |
* @param array $association | |
* @param object $owner | |
* @param User $user | |
*/ | |
protected function onInsert( | |
$collection, | |
array $association, | |
object $owner, | |
User $user | |
) { | |
foreach ($collection->getInsertDiff() as $entity) { | |
if (!$this->isSupportedClass($entity)) { | |
continue; | |
} | |
$logCollection = new LogCollection(); | |
$logCollection->setUser($user); | |
$logCollection->setLoggedAt(new \DateTime()); | |
$logCollection->setField($association['fieldName']); | |
$logCollection->setParentClass($association['sourceEntity']); | |
$logCollection->setChildClass(get_class($entity)); | |
$logCollection->setAction(LogCollection::ACTION_ADD); | |
if ($owner->getId()) { | |
$logCollection->setParentId($owner->getId()); | |
} | |
if ($entity->getId()) { | |
$logCollection->setChildId($entity->getId()); | |
} | |
if ($logCollection->getParentId() && $logCollection->getChildId()) { | |
$this->logCollections[$this->getHash($logCollection)] = $logCollection; | |
continue; | |
} | |
$ownerHash = spl_object_hash($owner); | |
if (!isset($this->parents[$ownerHash])) { | |
$this->parents[$ownerHash] = []; | |
} | |
$this->parents[$ownerHash][] = $logCollection; | |
$entityHash = spl_object_hash($entity); | |
if (!isset($this->children[$entityHash])) { | |
$this->children[$entityHash] = []; | |
} | |
$this->children[$entityHash][] = $logCollection; | |
} | |
} | |
/** | |
* When removing an item from a collection, store a logCollection action 'remove' in doctrine. | |
* | |
* @param PersistentCollection $collection | |
* @param array $association | |
* @param object $owner | |
* @param User $user | |
* @param EntityManagerInterface $em | |
* @param UnitOfWork $unitOfWork | |
*/ | |
protected function onDelete( | |
$collection, | |
array $association, | |
object $owner, | |
User $user, | |
EntityManagerInterface $em, | |
UnitOfWork $unitOfWork | |
) { | |
foreach ($collection->getDeleteDiff() as $entity) { | |
if (!$this->isSupportedClass($entity)) { | |
continue; | |
} | |
$logCollection = new LogCollection(); | |
$logCollection->setUser($user); | |
$logCollection->setLoggedAt(new \DateTime()); | |
$logCollection->setField($association['fieldName']); | |
$logCollection->setParentClass($association['sourceEntity']); | |
$logCollection->setChildClass(get_class($entity)); | |
$logCollection->setAction(LogCollection::ACTION_REMOVE); | |
$logCollection->setParentId($owner->getId()); | |
$logCollection->setChildId($entity->getId()); | |
$em->persist($logCollection); | |
$meta = $em->getClassMetadata(get_class($logCollection)); | |
$unitOfWork->computeChangeSet($meta, $logCollection); | |
$this->newLogCollections[$this->getHash($logCollection)] = $logCollection; | |
} | |
} | |
/** | |
* Sets LogCollection parentId after it has been persisted. | |
* | |
* @param LifecycleEventArgs $args | |
*/ | |
public function postPersist(LifecycleEventArgs $args) | |
{ | |
$entity = $args->getEntity(); | |
$this->updateId($entity); | |
} | |
/** | |
* Sets LogCollection parentId after it has been persisted. | |
* | |
* @param LogCollection $entity | |
*/ | |
protected function updateId(object $entity): void | |
{ | |
$oid = spl_object_hash($entity); | |
if (array_key_exists($oid, $this->parents)) { | |
foreach ($this->parents[$oid] as $logCollection) { | |
if (!$logCollection->getParentId()) { | |
$logCollection->setParentId($entity->getId()); | |
$this->updateLogCollectionInMemory($logCollection); | |
} | |
} | |
} | |
if (array_key_exists($oid, $this->children)) { | |
foreach ($this->children[$oid] as $logCollection) { | |
if (!$logCollection->getChildId()) { | |
$logCollection->setChildId($entity->getId()); | |
$this->updateLogCollectionInMemory($logCollection); | |
} | |
} | |
} | |
} | |
/** | |
* Update LogCollection in memory. | |
* | |
* @param LogCollection $logCollection | |
*/ | |
protected function updateLogCollectionInMemory(LogCollection $logCollection): void | |
{ | |
if ($logCollection->getParentId() && $logCollection->getChildId()) { | |
$hash = $this->getHash($logCollection); | |
if (!isset($this->logCollections[$hash])) { | |
$this->logCollections[$hash] = $logCollection; | |
} | |
} | |
} | |
/** | |
* Persists LogCollections after all other entities have been persisted. | |
* | |
* @param PostFlushEventArgs $args | |
*/ | |
public function postFlush(PostFlushEventArgs $args) | |
{ | |
$em = $args->getEntityManager(); | |
$logCollectionsFlushRequired = $this->postFlushLogCollections($em); | |
$newLogCollectionsFlushRequired = $this->postFlushNewLogCollections(); | |
if ($logCollectionsFlushRequired || $newLogCollectionsFlushRequired) { | |
$em->flush(); | |
} | |
} | |
/** | |
* Persists updated LogCollections after all other entities have been persisted. | |
* | |
* @param EntityManagerInterface $em | |
*/ | |
protected function postFlushLogCollections(EntityManagerInterface $em): bool | |
{ | |
$flushRequired = false; | |
if (0 < count($this->logCollections)) { | |
foreach ($this->logCollections as $logCollection) { | |
if (!$logCollection->getId()) { | |
$em->persist($logCollection); | |
$flushRequired = true; | |
$this->newLogCollections[$this->getHash($logCollection)] = $logCollection; | |
} | |
} | |
} | |
return $flushRequired; | |
} | |
/** | |
* Persists new LogCollections after all other entities have been persisted. | |
*/ | |
protected function postFlushNewLogCollections(): bool | |
{ | |
$flushRequired = false; | |
if (0 < count($this->newLogCollections)) { | |
foreach ($this->newLogCollections as $index => $logCollection) { | |
// Do final stuff here on $logCollection | |
// You have to check if $logCollection->getId() is defined or not | |
$flushRequired = true; | |
unset($this->newLogCollections[$index]); | |
} | |
} | |
return $flushRequired; | |
} | |
/** | |
* Returns unique hash based on LogCollection parent and child to avoid persisting duplicate. | |
* | |
* @param LogCollection $logCollection | |
* | |
* @return string | |
*/ | |
protected function getHash(LogCollection $logCollection) | |
{ | |
if (strcmp($logCollection->getParentClass(), $logCollection->getChildClass()) < 0) { | |
return sprintf( | |
'%s-%d-%s-%d', | |
$logCollection->getParentClass(), | |
$logCollection->getParentId(), | |
$logCollection->getChildClass(), | |
$logCollection->getChildId() | |
); | |
} | |
return sprintf( | |
'%s-%d-%s-%d', | |
$logCollection->getChildClass(), | |
$logCollection->getChildId(), | |
$logCollection->getParentClass(), | |
$logCollection->getParentId() | |
); | |
} | |
/** | |
* Blacklist some entity from being tracked. | |
* | |
* @param object $entity | |
*/ | |
protected function isSupportedClass($entity) | |
{ | |
return !in_array(ClassUtils::getRealClass(get_class($entity)), [ | |
LogCollection::class, | |
], true); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment