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:
2026-02-23 11:44:32 +01:00
parent 07417387bb
commit 3b8984eec5
5 changed files with 331 additions and 0 deletions

View File

@@ -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 {}

View File

@@ -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));
}
}

View File

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

View File

@@ -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 = [],
) {}
}