diff --git a/phpunit.xml b/phpunit.xml index 61d7944..eb7304f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,9 @@ - + diff --git a/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php b/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php index 794a7e8..6b141a5 100644 --- a/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php +++ b/src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php @@ -5,22 +5,27 @@ namespace DH\Auditor\Provider\Doctrine\Auditing\Event; use DH\Auditor\Provider\Doctrine\Auditing\DBAL\Middleware\AuditorDriver; +use DH\Auditor\Provider\Doctrine\Auditing\Transaction\AuditTrait; +use DH\Auditor\Provider\Doctrine\DoctrineProvider; use DH\Auditor\Provider\Doctrine\Model\Transaction; -use DH\Auditor\Transaction\TransactionManagerInterface; use Doctrine\Common\EventSubscriber; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; +use Doctrine\Persistence\Event\LifecycleEventArgs; +use Gedmo\SoftDeleteable\SoftDeleteableListener; final class DoctrineSubscriber implements EventSubscriber { + use AuditTrait; + /** @var Transaction[] */ private array $transactions = []; public function __construct( - private readonly TransactionManagerInterface $transactionManager, + private readonly DoctrineProvider $provider, private readonly EntityManagerInterface $entityManager ) {} @@ -38,7 +43,7 @@ public function onFlush(OnFlushEventArgs $args): void $transaction = ($this->transactions[$entityManagerId] ??= new Transaction($this->entityManager)); // Populate transaction - $this->transactionManager->populate($transaction); + $this->provider->getTransactionManager()->populate($transaction); $driver = $this->entityManager->getConnection()->getDriver(); @@ -48,15 +53,32 @@ public function onFlush(OnFlushEventArgs $args): void if ($driver instanceof AuditorDriver) { $driver->addFlusher(function () use ($transaction): void { - $this->transactionManager->process($transaction); + $this->provider->getTransactionManager()->process($transaction); $transaction->reset(); }); } } + public function postSoftDelete(LifecycleEventArgs $args): void + { + $entityManagerId = spl_object_id($this->entityManager); + + // cached transaction model, if it holds same EM no need to create a new one + $transaction = ($this->transactions[$entityManagerId] ??= new Transaction($this->entityManager)); + + if ($this->provider->isAudited($args->getObject())) { + $transaction->remove( + $args->getObject(), + $this->id($this->entityManager, $args->getObject()), + ); + } + } + public function getSubscribedEvents(): array { - return [Events::onFlush]; + return class_exists(SoftDeleteableListener::class) ? + [Events::onFlush, SoftDeleteableListener::POST_SOFT_DELETE] : + [Events::onFlush]; } /** diff --git a/src/Provider/Doctrine/DoctrineProvider.php b/src/Provider/Doctrine/DoctrineProvider.php index 8120750..0fdda41 100644 --- a/src/Provider/Doctrine/DoctrineProvider.php +++ b/src/Provider/Doctrine/DoctrineProvider.php @@ -59,6 +59,11 @@ public function __construct(ConfigurationInterface $configuration) $this->configuration->setProvider($this); } + public function getTransactionManager(): TransactionManager + { + return $this->transactionManager; + } + public function registerAuditingService(AuditingServiceInterface $service): ProviderInterface { parent::registerAuditingService($service); @@ -67,10 +72,10 @@ public function registerAuditingService(AuditingServiceInterface $service): Prov $entityManager = $service->getEntityManager(); $evm = $entityManager->getEventManager(); - // Register subscribers + // Register audit listeners and subscribers $evm->addEventListener([Events::loadClassMetadata], new TableSchemaListener($this)); $evm->addEventListener([ToolEvents::postGenerateSchemaTable], new CreateSchemaListener($this)); - $evm->addEventSubscriber(new DoctrineSubscriber($this->transactionManager, $entityManager)); + $evm->addEventSubscriber(new DoctrineSubscriber($this, $entityManager)); return $this; } diff --git a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php index e5998db..5e7f772 100644 --- a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php +++ b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php @@ -156,6 +156,65 @@ public function testRemove(): void ], $entry->getDiffs(), 'audit entry diffs is ok.'); } + public function testRemoveWithSoftDelete(): void + { + $processor = new TransactionProcessor($this->provider); + $reader = new Reader($this->provider); + $method = $this->reflectMethod(TransactionProcessor::class, 'remove'); + + /** @var StorageService $storageService */ + $storageService = $this->provider->getStorageServiceForEntity(Post::class); + $entityManager = $storageService->getEntityManager(); + + $post = new Post(); + $post + ->setId(1) + ->setTitle('First post') + ->setBody('Here is the body') + ->setCreatedAt(new \DateTimeImmutable('now')) + ; + + $method->invokeArgs($processor, [$entityManager, $post, 1, 'what-a-nice-transaction-hash']); + + // 1 "remove" audit entry added => count is 1 + $audits = $reader->createQuery(Post::class)->execute(); + $this->assertCount(1, $audits, 'TransactionProcessor::remove() creates an audit entry.'); + + $entry = array_shift($audits); + $this->assertSame(1, $entry->getId(), 'audit entry ID is ok.'); + $this->assertSame(Transaction::REMOVE, $entry->getType(), 'audit entry type is ok.'); + $this->assertContainsEquals($entry->getUserId(), ['1', '2'], 'audit entry blame_id is ok.'); + $this->assertContainsEquals($entry->getUsername(), ['dark.vador', 'anakin.skywalker'], 'audit entry blame_user is ok.'); + $this->assertSame('1.2.3.4', $entry->getIp(), 'audit entry IP is ok.'); + $this->assertSame([ + 'class' => Post::class, + 'id' => 1, + 'label' => 'First post', + 'table' => $entityManager->getClassMetadata(Post::class)->getTableName(), + ], $entry->getDiffs(), 'audit entry diffs is ok.'); + + $post = new Post(); + $post + ->setId(1) + ->setTitle('First post') + ->setBody('Here is the body') + ->setCreatedAt(new \DateTimeImmutable('now')) + ; + $entityManager->persist($post); + $entityManager->flush(); + + // 1 "remove" audit entry added => count is 2 + $audits = $reader->createQuery(Post::class)->execute(); + $this->assertCount(2, $audits, '1 "insert" audit entry added.'); + + $entityManager->remove($post); + $entityManager->flush(); + + // 1 "remove" audit entry added => count is 3 + $audits = $reader->createQuery(Post::class)->execute(); + $this->assertCount(3, $audits, '1 "remove" audit entry added.'); + } + public function testAssociateOneToMany(): void { $processor = new TransactionProcessor($this->provider); diff --git a/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php b/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php index eaaed75..2dc7704 100644 --- a/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php +++ b/tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php @@ -6,7 +6,7 @@ use DH\Auditor\Provider\Doctrine\Auditing\DBAL\Middleware\AuditorDriver; use DH\Auditor\Provider\Doctrine\Auditing\Event\DoctrineSubscriber; -use DH\Auditor\Transaction\TransactionManagerInterface; +use DH\Auditor\Tests\Provider\Doctrine\Traits\DoctrineProviderTrait; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection as ConnectionDbal; use Doctrine\DBAL\Driver; @@ -27,20 +27,10 @@ #[Small] final class DoctrineSubscriberTest extends TestCase { + use DoctrineProviderTrait; + public function testIssue184IfAbstractDriverMiddleware(): void { - $transactionManager = new class implements TransactionManagerInterface { - public function populate($transaction): void {} - - public function process($transaction): void - { - static $i = 0; - ++$i; - if ($i > 1) { - throw new \RuntimeException('Expected only once'); - } - } - }; $objectManager = $this->createMock(EntityManagerInterface::class); $args = new OnFlushEventArgs($objectManager); @@ -59,7 +49,11 @@ public function process($transaction): void ->willReturn($driver) ; - $target = new DoctrineSubscriber($transactionManager, $objectManager); + $provider = $this->createDoctrineProvider($this->createProviderConfiguration([ + 'entities' => [], + ])); + + $target = new DoctrineSubscriber($provider, $objectManager); $target->onFlush($args); foreach ($dhDriver->getFlusherList() as $item) { @@ -71,18 +65,6 @@ public function process($transaction): void public function testIssue184IfNotAbstractDriverMiddleware(): void { - $transactionManager = new class implements TransactionManagerInterface { - public function populate($transaction): void {} - - public function process($transaction): void - { - static $i = 0; - ++$i; - if ($i > 1) { - throw new \RuntimeException('Expected only once'); - } - } - }; $objectManager = $this->createMock(EntityManagerInterface::class); $args = new OnFlushEventArgs($objectManager); @@ -140,7 +122,11 @@ public function getExceptionConverter(): Driver\API\ExceptionConverter ->willReturn($driver) ; - $target = new DoctrineSubscriber($transactionManager, $objectManager); + $provider = $this->createDoctrineProvider($this->createProviderConfiguration([ + 'entities' => [], + ])); + + $target = new DoctrineSubscriber($provider, $objectManager); $target->onFlush($args); foreach ($auditorDriver->getFlusherList() as $item) { @@ -152,14 +138,6 @@ public function getExceptionConverter(): Driver\API\ExceptionConverter public function testIssue184Unexpected(): void { - $transactionManager = new class implements TransactionManagerInterface { - public function populate($transaction): void {} - - public function process($transaction): void - { - throw new \RuntimeException('Unexpected call'); - } - }; $objectManager = $this->createMock(EntityManagerInterface::class); $args = new OnFlushEventArgs($objectManager); @@ -222,7 +200,11 @@ public function getExceptionConverter(): Driver\API\ExceptionConverter ->willReturn($configuration = $this->createMock(Configuration::class)) ; - $target = new DoctrineSubscriber($transactionManager, $objectManager); + $provider = $this->createDoctrineProvider($this->createProviderConfiguration([ + 'entities' => [], + ])); + + $target = new DoctrineSubscriber($provider, $objectManager); $target->onFlush($args); $this->assertTrue(true); diff --git a/tests/Provider/Doctrine/Traits/EntityManagerInterfaceTrait.php b/tests/Provider/Doctrine/Traits/EntityManagerInterfaceTrait.php index 484302b..119cfa5 100644 --- a/tests/Provider/Doctrine/Traits/EntityManagerInterfaceTrait.php +++ b/tests/Provider/Doctrine/Traits/EntityManagerInterfaceTrait.php @@ -4,10 +4,13 @@ namespace DH\Auditor\Tests\Provider\Doctrine\Traits; +use Doctrine\Common\EventManager; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\UnderscoreNamingStrategy; use Doctrine\ORM\ORMSetup; +use Gedmo\SoftDeleteable\SoftDeleteableListener; trait EntityManagerInterfaceTrait { @@ -27,12 +30,9 @@ private function createEntityManager(?array $paths = null, string $connectionNam $em = new EntityManager($connection, $configuration); $evm = $em->getEventManager(); - $allListeners = method_exists($evm, 'getAllListeners') ? $evm->getAllListeners() : $evm->getListeners(); - foreach ($allListeners as $event => $listeners) { - foreach ($listeners as $listener) { - $evm->removeEventListener([$event], $listener); - } - } + + // Attach SoftDeleteableListener to the EventManager + $evm->addEventListener(Events::onFlush, new SoftDeleteableListener()); return $em; }