From a0796852dd8a9f2f8f2b9ec1075970406931f14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 17 Feb 2026 18:49:14 +0100 Subject: [PATCH] Add support for audit event dumping and improve subject rendering flexibility - Added `AuditEventDumper` class to generate and store CSV exports of audit events based on search criteria. - Updated `SubjectDisplayerInterface` and related classes to support multiple output formats (`html` or `string`) for subject rendering. --- .../Audit/AuditEventDumper.php | 132 ++++++++++++++++++ .../Audit/SubjectConverterManager.php | 6 +- .../SubjectConverterManagerInterface.php | 5 +- .../Audit/SubjectDisplayerInterface.php | 4 +- .../Audit/Twig/SubjectRenderRuntimeTwig.php | 1 + 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php diff --git a/src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php b/src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php new file mode 100644 index 000000000..765de194b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php @@ -0,0 +1,132 @@ +auditTrailRepository->findByCriteriaIterator($criteria) as $audit) { + \assert($audit instanceof AuditTrail); + + $occurredAt = $formatter?->format($audit->getOccurredAt()) ?: $audit->getOccurredAt()->format('c'); + + $user = $audit->getUser(); + $userId = $user?->getId(); + $userLabel = $user ? $this->userRender->renderString($user, ['at_date' => $audit->getOccurredAt()]) : ''; + + // Displayable name of the audit (main subject) + $mainSubjectDisplay = ''; + $mainSubject = $audit->getMainSubject(); + if (null !== $mainSubject) { + $mainSubjectDisplay = $this->converterManager->display(Subject::fromArray($mainSubject), 'string'); + } + + // Translated action + $action = $this->translator->trans('audit_trail.action.'.$audit->getAction(), [], null, $lang); + + // Metadata JSON + $metadata = json_encode($audit->getMetadata(), JSON_THROW_ON_ERROR); + + // Associated subjects, comma separated + $subjectsDisplay = ''; + $subjects = $audit->getSubjects(); + if (!empty($subjects)) { + $subjectsDisplay = implode(', ', array_map(function (array $s): string { + return $this->converterManager->display(Subject::fromArray($s), 'fr'); + }, $subjects)); + } + + fputcsv($handle, [ + $occurredAt, + (string) ($userId ?? ''), + $userLabel, + $mainSubjectDisplay, + $action, + $audit->getDescription(), + $metadata, + $subjectsDisplay, + ]); + + ++$i; + if (0 === $i % 100) { + // free memory from managed entities every 100 lines + $this->entityManager->clear(); + } + } + + fclose($handle); + + // Create a stored object, set auto-removal after 24h, and store content + $storedObject = new StoredObject(); + $storedObject->setDeleteAt((new \DateTimeImmutable('now'))->modify('+24 hours')); + + // Persist stored object so that a valid identifier exists in DB + $this->entityManager->persist($storedObject); + $this->entityManager->flush(); + + $content = file_get_contents($tmpPath) ?: ''; + $this->storedObjectManager->write($storedObject, $content, 'text/csv'); + + // Remove temp file from disk + @unlink($tmpPath); + + return $storedObject; + } +} diff --git a/src/Bundle/ChillMainBundle/Audit/SubjectConverterManager.php b/src/Bundle/ChillMainBundle/Audit/SubjectConverterManager.php index fdda6a6ab..dedfd4ea3 100644 --- a/src/Bundle/ChillMainBundle/Audit/SubjectConverterManager.php +++ b/src/Bundle/ChillMainBundle/Audit/SubjectConverterManager.php @@ -43,11 +43,11 @@ final readonly class SubjectConverterManager implements SubjectConverterManagerI throw new ConvertSubjectException($subject); } - public function display(Subject $subject, array $options = []): string + public function display(Subject $subject, string $format = 'html', array $options = []): string { foreach ($this->displayers as $displayer) { - if ($displayer->supportsDisplay($subject, $options = [])) { - return $displayer->display($subject, $options = []); + if ($displayer->supportsDisplay($subject)) { + return $displayer->display($subject, $format, $options); } } diff --git a/src/Bundle/ChillMainBundle/Audit/SubjectConverterManagerInterface.php b/src/Bundle/ChillMainBundle/Audit/SubjectConverterManagerInterface.php index 5925cf680..86294ff46 100644 --- a/src/Bundle/ChillMainBundle/Audit/SubjectConverterManagerInterface.php +++ b/src/Bundle/ChillMainBundle/Audit/SubjectConverterManagerInterface.php @@ -15,5 +15,8 @@ interface SubjectConverterManagerInterface { public function getSubjectsForEntity(object $subject, bool $includeAssociated = false): SubjectBag; - public function display(Subject $subject): string; + /** + * @param 'html'|'string' $format + */ + public function display(Subject $subject, string $format = 'html', array $options = []): string; } diff --git a/src/Bundle/ChillMainBundle/Audit/SubjectDisplayerInterface.php b/src/Bundle/ChillMainBundle/Audit/SubjectDisplayerInterface.php index 63e24ff5c..1408afea9 100644 --- a/src/Bundle/ChillMainBundle/Audit/SubjectDisplayerInterface.php +++ b/src/Bundle/ChillMainBundle/Audit/SubjectDisplayerInterface.php @@ -21,7 +21,9 @@ interface SubjectDisplayerInterface /** * Render a Subject as an HTML string. * + * @param 'html'|'string' $format the output format + * * @return string an html string */ - public function display(Subject $subject, array $options = []): string; + public function display(Subject $subject, string $format = 'html', array $options = []): string; } diff --git a/src/Bundle/ChillMainBundle/Audit/Twig/SubjectRenderRuntimeTwig.php b/src/Bundle/ChillMainBundle/Audit/Twig/SubjectRenderRuntimeTwig.php index 068e2b818..1c3f19b91 100644 --- a/src/Bundle/ChillMainBundle/Audit/Twig/SubjectRenderRuntimeTwig.php +++ b/src/Bundle/ChillMainBundle/Audit/Twig/SubjectRenderRuntimeTwig.php @@ -23,6 +23,7 @@ final readonly class SubjectRenderRuntimeTwig implements RuntimeExtensionInterfa { return $this->subjectConverterManager->display( $subject instanceof Subject ? $subject : Subject::fromArray($subject), + 'html', $options ); }