Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensures soft-delete is audited #224

Merged
merged 2 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading