Skip to content

Instantly share code, notes, and snippets.

@whizzrd
Forked from ThomasLabstep/LogCollection.php
Created August 10, 2022 04:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save whizzrd/3f67928e84fe29a779e30988fba5a5e5 to your computer and use it in GitHub Desktop.
Save whizzrd/3f67928e84fe29a779e30988fba5a5e5 to your computer and use it in GitHub Desktop.
Gedmo Loggable - Collection
<?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;
}
<?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