mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-03 04:29:40 +00:00
Add AuditDumpRequestHandler to process audit dump requests and ensure transactional consistency
- Introduced `AuditDumpRequestHandler` to handle `AuditDumpRequestMessage` with transactional safety. - Added custom exceptions `AuditDumpTooMuchLines` and `AuditDumpAlreadyGeneratedException` for enhanced error reporting during processing. - Created unit tests in `AuditDumpRequestHandlerTest` to validate normal processing, exception handling, and edge cases. - Implemented locking mechanism and state management for `StoredObject` while processing requests.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Audit\Exception;
|
||||
|
||||
class AuditDumpAlreadyGeneratedException extends \RuntimeException {}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Audit\Exception;
|
||||
|
||||
class AuditDumpTooMuchLines extends \RuntimeException
|
||||
{
|
||||
public function __construct(int $lines, int $maxLines)
|
||||
{
|
||||
parent::__construct(sprintf('Audit dump contains too much lines (%d) compared to maximum allowed (%d)', $lines, $maxLines));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Audit\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\MainBundle\Audit\AuditEventDumper;
|
||||
use Chill\MainBundle\Audit\Exception\AuditDumpAlreadyGeneratedException;
|
||||
use Chill\MainBundle\Audit\Exception\AuditDumpTooMuchLines;
|
||||
use Chill\MainBundle\Audit\Subject;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
final readonly class AuditDumpRequestHandler implements MessageHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuditEventDumper $auditEventDumper,
|
||||
private StoredObjectRepositoryInterface $storedObjectRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UserRepositoryInterface $userRepository,
|
||||
) {}
|
||||
|
||||
public function __invoke(AuditDumpRequestMessage $message): void
|
||||
{
|
||||
$conn = $this->entityManager->getConnection();
|
||||
$beganTx = false;
|
||||
if (!$conn->isTransactionActive()) {
|
||||
$conn->beginTransaction();
|
||||
$beganTx = true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Lock the StoredObject until the end of the process
|
||||
$storedObject = $this->entityManager->find(StoredObject::class, $message->storedObjectId, LockMode::PESSIMISTIC_WRITE);
|
||||
if (!$storedObject instanceof StoredObject) {
|
||||
// Nothing to do if stored object does not exist anymore
|
||||
if ($beganTx) {
|
||||
$conn->commit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Build criteria expected by AuditTrailRepository
|
||||
$criteria = [];
|
||||
if (null !== $message->from) {
|
||||
$criteria['from_date'] = $message->from;
|
||||
}
|
||||
if (null !== $message->to) {
|
||||
$criteria['to_date'] = $message->to;
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
foreach ($message->subjects as $s) {
|
||||
if (is_array($s)) {
|
||||
$subjects[] = Subject::fromArray($s);
|
||||
}
|
||||
}
|
||||
if ([] !== $subjects) {
|
||||
$criteria['subjects'] = $subjects;
|
||||
}
|
||||
|
||||
$byUsers = [];
|
||||
foreach ($message->byUsers as $userId) {
|
||||
$byUsers[] = $this->userRepository->find($userId);
|
||||
}
|
||||
if ([] !== $byUsers) {
|
||||
$criteria['by_users'] = $byUsers;
|
||||
}
|
||||
|
||||
$this->auditEventDumper->dump($criteria, $message->lang, $storedObject);
|
||||
|
||||
$this->entityManager->flush();
|
||||
if ($beganTx) {
|
||||
$conn->commit();
|
||||
}
|
||||
} catch (AuditDumpTooMuchLines|AuditDumpAlreadyGeneratedException $e) {
|
||||
if ($beganTx) {
|
||||
$this->entityManager->flush();
|
||||
$conn->commit();
|
||||
}
|
||||
|
||||
throw new UnrecoverableMessageHandlingException(previous: $e);
|
||||
} catch (\Throwable $e) {
|
||||
if ($beganTx && $conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
} finally {
|
||||
// Clear the EntityManager state at the end
|
||||
$this->entityManager->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Audit\Messenger;
|
||||
|
||||
final readonly class AuditDumpRequestMessage
|
||||
{
|
||||
public function __construct(
|
||||
public string $lang,
|
||||
public int $storedObjectId,
|
||||
public ?\DateTimeImmutable $from = null,
|
||||
public ?\DateTimeImmutable $to = null,
|
||||
public array $subjects = [],
|
||||
public array $byUsers = [],
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Tests\Audit\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface;
|
||||
use Chill\MainBundle\Audit\AuditEventDumper;
|
||||
use Chill\MainBundle\Audit\Exception\AuditDumpAlreadyGeneratedException;
|
||||
use Chill\MainBundle\Audit\Exception\AuditDumpTooMuchLines;
|
||||
use Chill\MainBundle\Audit\Messenger\AuditDumpRequestHandler;
|
||||
use Chill\MainBundle\Audit\Messenger\AuditDumpRequestMessage;
|
||||
use Chill\MainBundle\Audit\Subject;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AuditDumpRequestHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private $auditEventDumper;
|
||||
private $storedObjectRepository;
|
||||
private $entityManager;
|
||||
private $userRepository;
|
||||
private $connection;
|
||||
private $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->auditEventDumper = $this->prophesize(AuditEventDumper::class);
|
||||
$this->storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class);
|
||||
$this->entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->userRepository = $this->prophesize(UserRepositoryInterface::class);
|
||||
$this->connection = $this->prophesize(Connection::class);
|
||||
|
||||
$this->entityManager->getConnection()->willReturn($this->connection->reveal());
|
||||
|
||||
$this->handler = new AuditDumpRequestHandler(
|
||||
$this->auditEventDumper->reveal(),
|
||||
$this->storedObjectRepository->reveal(),
|
||||
$this->entityManager->reveal(),
|
||||
$this->userRepository->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
public function testInvokeNormalFlow(): void
|
||||
{
|
||||
$message = new AuditDumpRequestMessage(
|
||||
lang: 'fr',
|
||||
storedObjectId: 123,
|
||||
from: new \DateTimeImmutable('2023-01-01'),
|
||||
to: new \DateTimeImmutable('2023-01-31'),
|
||||
subjects: [['t' => 'person', 'id' => 456]],
|
||||
byUsers: [1]
|
||||
);
|
||||
|
||||
$storedObject = new StoredObject();
|
||||
$user = $this->prophesize(User::class)->reveal();
|
||||
|
||||
$this->connection->isTransactionActive()->willReturn(false);
|
||||
$this->connection->beginTransaction()->shouldBeCalled();
|
||||
|
||||
$this->entityManager->find(StoredObject::class, 123, LockMode::PESSIMISTIC_WRITE)
|
||||
->willReturn($storedObject);
|
||||
|
||||
$this->userRepository->find(1)->willReturn($user);
|
||||
|
||||
$expectedCriteria = [
|
||||
'from_date' => $message->from,
|
||||
'to_date' => $message->to,
|
||||
'subjects' => [Subject::fromArray(['t' => 'person', 'id' => 456])],
|
||||
'by_users' => [$user],
|
||||
];
|
||||
|
||||
$this->auditEventDumper->dump(
|
||||
Argument::that(function ($criteria) use ($expectedCriteria) {
|
||||
return $criteria['from_date'] === $expectedCriteria['from_date']
|
||||
&& $criteria['to_date'] === $expectedCriteria['to_date']
|
||||
&& 1 === count($criteria['subjects'])
|
||||
&& $criteria['subjects'][0]->isEqual($expectedCriteria['subjects'][0])
|
||||
&& $criteria['by_users'] === $expectedCriteria['by_users'];
|
||||
}),
|
||||
'fr',
|
||||
$storedObject
|
||||
)->shouldBeCalled();
|
||||
|
||||
$this->connection->commit()->shouldBeCalled();
|
||||
$this->entityManager->flush()->shouldBeCalled();
|
||||
$this->entityManager->clear()->shouldBeCalled();
|
||||
|
||||
($this->handler)($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExceptions
|
||||
*/
|
||||
public function testInvokeWithExceptions(\Exception $exception): void
|
||||
{
|
||||
$message = new AuditDumpRequestMessage('fr', 123);
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$this->connection->isTransactionActive()->willReturn(false, true);
|
||||
$this->connection->beginTransaction()->shouldBeCalled();
|
||||
|
||||
$this->entityManager->find(StoredObject::class, 123, LockMode::PESSIMISTIC_WRITE)
|
||||
->willReturn($storedObject);
|
||||
|
||||
$this->auditEventDumper->dump(Argument::any(), 'fr', $storedObject)
|
||||
->willThrow($exception);
|
||||
|
||||
$this->connection->commit()->shouldBeCalled();
|
||||
$this->entityManager->flush()->shouldBeCalled();
|
||||
$this->entityManager->clear()->shouldBeCalled();
|
||||
|
||||
$this->expectException(UnrecoverableMessageHandlingException::class);
|
||||
try {
|
||||
($this->handler)($message);
|
||||
} catch (UnrecoverableMessageHandlingException $e) {
|
||||
$this->assertSame($exception, $e->getPrevious());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function provideExceptions(): array
|
||||
{
|
||||
return [
|
||||
[new AuditDumpTooMuchLines(1000, 500)],
|
||||
[new AuditDumpAlreadyGeneratedException('Already generated')],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInvokeStoredObjectNotFound(): void
|
||||
{
|
||||
$message = new AuditDumpRequestMessage('fr', 123);
|
||||
|
||||
$this->connection->isTransactionActive()->willReturn(false);
|
||||
$this->connection->beginTransaction()->shouldBeCalled();
|
||||
|
||||
$this->entityManager->find(StoredObject::class, 123, LockMode::PESSIMISTIC_WRITE)
|
||||
->willReturn(null);
|
||||
|
||||
$this->connection->commit()->shouldBeCalled();
|
||||
$this->entityManager->clear()->shouldBeCalled();
|
||||
|
||||
($this->handler)($message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user