mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-03 12:39:42 +00:00
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.
This commit is contained in:
132
src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php
Normal file
132
src/Bundle/ChillMainBundle/Audit/AuditEventDumper.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?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;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Repository\AuditTrailRepository;
|
||||
use Chill\MainBundle\Entity\AuditTrail;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class AuditEventDumper
|
||||
{
|
||||
public function __construct(
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private AuditTrailRepository $auditTrailRepository,
|
||||
private TranslatorInterface $translator,
|
||||
private SubjectConverterManagerInterface $converterManager,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UserRender $userRender,
|
||||
) {}
|
||||
|
||||
public function dump(array $criteria, string $lang): StoredObject
|
||||
{
|
||||
// Create a temporary file on disk (not in memory)
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'audit_export_');
|
||||
if (false === $tmpPath) {
|
||||
throw new \RuntimeException('Unable to create a temporary file for audit dump');
|
||||
}
|
||||
|
||||
$handle = fopen($tmpPath, 'wb');
|
||||
if (false === $handle) {
|
||||
throw new \RuntimeException('Unable to open temporary file for writing');
|
||||
}
|
||||
|
||||
// CSV header (optional, but helpful)
|
||||
fputcsv($handle, [
|
||||
'occurred_at',
|
||||
'user_id',
|
||||
'user_label',
|
||||
'audit_subject',
|
||||
'action',
|
||||
'description',
|
||||
'metadata',
|
||||
'associated_subjects',
|
||||
]);
|
||||
|
||||
$formatter = \IntlDateFormatter::create(
|
||||
$lang,
|
||||
\IntlDateFormatter::SHORT,
|
||||
\IntlDateFormatter::LONG,
|
||||
);
|
||||
|
||||
$i = 0;
|
||||
foreach ($this->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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ final readonly class SubjectRenderRuntimeTwig implements RuntimeExtensionInterfa
|
||||
{
|
||||
return $this->subjectConverterManager->display(
|
||||
$subject instanceof Subject ? $subject : Subject::fromArray($subject),
|
||||
'html',
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user