Skip to content

Commit

Permalink
Ensures soft-delete is audited (#224)
Browse files Browse the repository at this point in the history
* Ensures soft-delete is audited

* Safety check in case gedmo/doctrine-extensions not installed
  • Loading branch information
DamienHarper authored Oct 15, 2024
1 parent f40db64 commit e6909e5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 51 deletions.
4 changes: 2 additions & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd" bootstrap="vendor/autoload.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.4/phpunit.xsd" bootstrap="vendor/autoload.php"
executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" cacheDirectory=".phpunit.cache"
requireCoverageMetadata="false" beStrictAboutCoverageMetadata="true">
<coverage includeUncoveredFiles="true">
<coverage>
<report>
<html outputDirectory="tests/coverage"/>
</report>
Expand Down
32 changes: 27 additions & 5 deletions src/Provider/Doctrine/Auditing/Event/DoctrineSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}

Expand All @@ -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();

Expand All @@ -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];
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/Provider/Doctrine/DoctrineProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 18 additions & 36 deletions tests/Provider/Doctrine/Event/DoctrineSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions tests/Provider/Doctrine/Traits/EntityManagerInterfaceTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
}
Expand Down

0 comments on commit e6909e5

Please sign in to comment.