From b89911e307c0ef82bd0a704c8fd5cc8ebe19f10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 17 Feb 2026 12:04:22 +0100 Subject: [PATCH] Add `RemoveOldAuditCronJob` to clean up outdated audit trails - Introduced `RemoveOldAuditCronJob` class implementing `CronJobInterface` to delete old audit trails based on configured retention period. - Added `deleteBefore` method to `AuditTrailRepository` to handle removal of records older than the specified date. - Created `RemoveOldAuditCronJobTest` to ensure correct functionality for job execution and canRun logic. - Updated DI configuration to include the `delete_after` parameter for audit trail retention settings. --- .../Audit/RemoveOldAuditCronJob.php | 58 +++++++++++++++ .../ChillMainExtension.php | 2 + .../DependencyInjection/Configuration.php | 6 ++ .../Repository/AuditTrailRepository.php | 9 +++ .../Tests/Audit/RemoveOldAuditCronJobTest.php | 72 +++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Audit/RemoveOldAuditCronJob.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Audit/RemoveOldAuditCronJobTest.php diff --git a/src/Bundle/ChillMainBundle/Audit/RemoveOldAuditCronJob.php b/src/Bundle/ChillMainBundle/Audit/RemoveOldAuditCronJob.php new file mode 100644 index 000000000..c1cc804d2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Audit/RemoveOldAuditCronJob.php @@ -0,0 +1,58 @@ +get('chill_main.audit_trail'); + if (is_array($config) && is_string($intervalString = $config['delete_after'] ?? null)) { + $this->deleteBefore = new \DateInterval($intervalString); + } + } + + public function canRun(?CronJobExecution $cronJobExecution): bool + { + if (null === $cronJobExecution) { + return true; + } + + return $this->clock->now() >= $cronJobExecution->getLastStart()->add(new \DateInterval('PT23H58M')); + } + + public function getKey(): string + { + return self::KEY; + } + + public function run(array $lastExecutionData): ?array + { + $deleteBefore = $this->clock->now()->sub($this->deleteBefore); + $this->auditTrailRepository->deleteBefore($deleteBefore); + + return ['delete-before' => $deleteBefore->format(\DateTimeImmutable::ISO8601_EXPANDED)]; + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 369f38203..ed1b1661d 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -211,6 +211,8 @@ class ChillMainExtension extends Extension implements $config['top_banner'] ?? [] ); + $container->setParameter('chill_main.audit_trail', $config['audit_trail']); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/doctrine.yaml'); diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index a3247d88e..a9f3c141b 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -325,6 +325,12 @@ class Configuration implements ConfigurationInterface ->end() ->end(); + $rootNode->children() + ->arrayNode('audit_trail')->addDefaultsIfNotSet()->children() + ->scalarNode('delete_after')->cannotBeEmpty()->defaultValue('P6M')->info('The duration (a valid interval) before deleting the audit trail. Will be run by a cronjob.')->end() + ->end() + ->end(); + return $treeBuilder; } } diff --git a/src/Bundle/ChillMainBundle/Repository/AuditTrailRepository.php b/src/Bundle/ChillMainBundle/Repository/AuditTrailRepository.php index b839be1d4..d7610b602 100644 --- a/src/Bundle/ChillMainBundle/Repository/AuditTrailRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/AuditTrailRepository.php @@ -85,6 +85,15 @@ class AuditTrailRepository extends ServiceEntityRepository ->getQuery()->getSingleScalarResult(); } + public function deleteBefore(\DateTimeImmutable $date): void + { + $this->createQueryBuilder('audit') + ->delete() + ->where('audit.occurredAt < :date') + ->setParameter('date', $date, Types::DATETIMETZ_IMMUTABLE) + ->getQuery()->execute(); + } + /** * @param array{subjects?: list, from_date?: \DateTimeImmutable, to_date?: \DateTimeImmutable, by_users?: list} $criteria */ diff --git a/src/Bundle/ChillMainBundle/Tests/Audit/RemoveOldAuditCronJobTest.php b/src/Bundle/ChillMainBundle/Tests/Audit/RemoveOldAuditCronJobTest.php new file mode 100644 index 000000000..a3be3b5f8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Audit/RemoveOldAuditCronJobTest.php @@ -0,0 +1,72 @@ + ['delete_after' => 'P1Y']]); + $repository = $this->prophesize(AuditTrailRepository::class); + + $job = new RemoveOldAuditCronJob($bag, $repository->reveal(), $clock); + + // with null value (must return true) + self::assertTrue($job->canRun(null)); + + // with a last execution 23h ago (must return false) + $execution23h = new CronJobExecution('key'); + $execution23h->setLastStart($now->sub(new \DateInterval('PT23H'))); + self::assertFalse($job->canRun($execution23h), 'Should return false when last execution was 23h ago'); + + // with a last execution 25h ago (must return true) + $execution25h = new CronJobExecution('key'); + $execution25h->setLastStart($now->sub(new \DateInterval('PT25H'))); + self::assertTrue($job->canRun($execution25h), 'Should return true when last execution was 25h ago'); + } + + public function testRun(): void + { + $now = new \DateTimeImmutable('2024-01-01 12:00:00'); + $clock = new MockClock($now); + $bag = new ParameterBag(['chill_main.audit_trail' => ['delete_after' => 'P1M']]); + $repository = $this->prophesize(AuditTrailRepository::class); + + $expectedDeleteBefore = $now->sub(new \DateInterval('P1M')); + + // add a spy and check that the deleteBefore method is triggered + $repository->deleteBefore($expectedDeleteBefore)->shouldBeCalled(); + + $job = new RemoveOldAuditCronJob($bag, $repository->reveal(), $clock); + $result = $job->run([]); + + self::assertArrayHasKey('delete-before', $result); + self::assertEquals($expectedDeleteBefore->format(\DateTimeImmutable::ISO8601_EXPANDED), $result['delete-before']); + } +}