mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-03 20:49:41 +00:00
- Added `audit.md` to explain how to trigger audits, best practices, and internal architecture. - Updated `mkdocs.yml` to include the new "Audit" section in the developer guide.
213 lines
8.6 KiB
Markdown
213 lines
8.6 KiB
Markdown
# Audit — developer guide
|
|
|
|
This page explains how to trigger an audit from your code, recommended practices, how subjects are converted and displayed, and how the whole pipeline persists an `AuditTrail`.
|
|
|
|
## Triggering an audit
|
|
|
|
Audits are triggered through the `Chill\MainBundle\Audit\TriggerAuditInterface` service. It exposes a single method and is also invokable:
|
|
|
|
```php
|
|
use Chill\MainBundle\Audit\TriggerAuditInterface;
|
|
use Chill\MainBundle\Entity\AuditTrail; // for action constants
|
|
use Symfony\Component\Translation\TranslatableMessage;
|
|
|
|
final class MyController
|
|
{
|
|
public function __construct(private readonly TriggerAuditInterface $triggerAudit) {}
|
|
|
|
public function someAction(MyEntity $entity): Response
|
|
{
|
|
// ... persist/flush $entity so that its id is available
|
|
|
|
$this->triggerAudit->triggerAudit(
|
|
AuditTrail::AUDIT_UPDATE, // $action
|
|
$entity, // $mainSubject (object)
|
|
subjects: [$entity->getOwner()], // optional related subjects (array of objects)
|
|
description: new TranslatableMessage('audit.entity.updated'), // optional description
|
|
metadata: ['field' => 'value'] // optional machine-readable extras
|
|
);
|
|
|
|
// equivalently (because the service is invokable):
|
|
($this->triggerAudit)(AuditTrail::AUDIT_UPDATE, $entity);
|
|
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
- `$action`: an action label such as `AuditTrail::AUDIT_CREATE`, `AUDIT_UPDATE`, `AUDIT_DELETE`, `AUDIT_VIEW`, `AUDIT_LIST`, ...
|
|
- `$mainSubject`: the primary domain object the action is about.
|
|
- `$subjects`: additional related domain objects to attach to the audit (optional). The always-related subject should be added through the converter.
|
|
- `$description`: a short human-readable text, already translated. You may pass a plain string or any `TranslatableInterface` (e.g. `TranslatableMessage`).
|
|
- `$metadata`: free-form associative array to add machine-readable context (ids, filters, counters, etc.).
|
|
|
|
!!! warning
|
|
|
|
The id of every subject must be set when you trigger the audit. In practice this means:
|
|
- trigger audits after persist operations (once Doctrine assigned an id, i.e. after `flush()` when needed), and
|
|
- trigger audits before delete operations (while the id is still available).
|
|
|
|
See concrete usages in controllers such as:
|
|
- `Chill\PersonBundle\Controller\AccompanyingCourseApiController` (multiple calls to `$this->triggerAudit->triggerAudit(...)`).
|
|
|
|
Example extracted and simplified from `AccompanyingCourseApiController`:
|
|
|
|
```php
|
|
$this->managerRegistry->getManager()->flush(); // ensure ids are set
|
|
|
|
$this->triggerAudit->triggerAudit(
|
|
AuditTrail::AUDIT_UPDATE,
|
|
$accompanyingPeriod,
|
|
description: new TranslatableMessage('audit.accompanying_period.set_requestor'),
|
|
metadata: ['t' => $accompanyingPeriod->getRequestorKind(), 'requestor' => $accompanyingPeriod->getRequestor()->getId()],
|
|
);
|
|
```
|
|
|
|
### Best practices
|
|
|
|
- Audit every create, update, delete, view and list action on domain entities.
|
|
- Use only the `$action` when it is obvious and self-explanatory.
|
|
- Use the `$description` when the action alone is not enough. For example for a list, the main subject is the person (or the accompanying period) the list is attached to, and the description should summarize the list content (filters, ranges, counts, …).
|
|
- Add `$metadata` to provide extra, machine-friendly details (ids, filters, pagination, counts, etc.).
|
|
- Reuse the existing patterns found in current controllers and services.
|
|
|
|
Examples:
|
|
|
|
```php
|
|
// CREATE a person
|
|
($this->triggerAudit)(AuditTrail::AUDIT_CREATE, $person);
|
|
|
|
// VIEW a person details
|
|
($this->triggerAudit)(AuditTrail::AUDIT_VIEW, $person);
|
|
|
|
// LIST resources attached to an accompanying period with filters in metadata
|
|
($this->triggerAudit)(
|
|
AuditTrail::AUDIT_LIST,
|
|
$accompanyingPeriod,
|
|
description: 'Listing resources for the current period',
|
|
metadata: ['scope' => 'resources', 'filters' => ['category' => 'housing', 'page' => 1]]
|
|
);
|
|
```
|
|
|
|
|
|
## Use new entities in Audits
|
|
|
|
### Creating a new SubjectConverter
|
|
|
|
Subjects are normalized into a serializable `Subject` model using `Chill\MainBundle\Audit\SubjectConverterInterface`.
|
|
Implement a converter when you introduce a new domain type you want to appear in audits.
|
|
|
|
Interface:
|
|
|
|
```php
|
|
interface SubjectConverterInterface
|
|
{
|
|
/**
|
|
* @param object $subject
|
|
*/
|
|
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag;
|
|
|
|
/** @phpstan-assert-if-true T $subject */
|
|
public function supportsConvert(mixed $subject): bool;
|
|
|
|
public static function getDefaultPriority(): int;
|
|
}
|
|
```
|
|
|
|
Typical implementation skeleton:
|
|
|
|
```php
|
|
use Chill\MainBundle\Audit\{Subject, SubjectBag, SubjectConverterInterface, SubjectConverterManagerAwareInterface, SubjectConverterManagerAwareTrait};
|
|
|
|
final class MyEntitySubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface
|
|
{
|
|
use SubjectConverterManagerAwareTrait;
|
|
|
|
public function supportsConvert(mixed $subject): bool
|
|
{
|
|
return $subject instanceof MyEntity;
|
|
}
|
|
|
|
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
|
|
{
|
|
\assert($subject instanceof MyEntity);
|
|
|
|
$main = new SubjectBag(
|
|
new Subject(
|
|
type: 'my_entity',
|
|
identifiers: ['id' => $subject->getId()],
|
|
)
|
|
);
|
|
|
|
$associated = [];
|
|
if ($includeAssociated && null !== $subject->getOwner()) {
|
|
// we delegate the conversion of the owner to the manager
|
|
$main->append($this->subjectConverterManager->getSubjectsForEntity($subject->getOwner(), false));
|
|
}
|
|
|
|
return new SubjectBag($main, $associated);
|
|
}
|
|
|
|
public static function getDefaultPriority(): int
|
|
{
|
|
return 0; // higher value = earlier pick when multiple converters support the same type
|
|
}
|
|
}
|
|
```
|
|
|
|
- `SubjectBag` groups the primary `Subject` and optional associated `Subject`s.
|
|
- Converters are discovered and composed by `SubjectConverterManagerInterface`.
|
|
- Implement `SubjectConverterManagerAwareInterface` (and use `SubjectConverterManagerAwareTrait`) if your converter needs to delegate the conversion of associated entities to the manager (useful if there is already a converter for that entity).
|
|
|
|
### Creating a new SubjectDisplayer
|
|
|
|
To control how a `Subject` is rendered in the UI (HTML or plain string), implement `Chill\MainBundle\Audit\SubjectDisplayerInterface`:
|
|
|
|
```php
|
|
interface SubjectDisplayerInterface
|
|
{
|
|
public function supportsDisplay(Subject $subject, array $options = []): bool;
|
|
|
|
/** @param 'html'|'string' $format */
|
|
public function display(Subject $subject, string $format = 'html', array $options = []): string;
|
|
}
|
|
```
|
|
|
|
Example: see `Chill\PersonBundle\Audit\Displayer\HouseholdCompositionDisplayer`. A simplified version:
|
|
|
|
```php
|
|
use Chill\MainBundle\Audit\{Subject, SubjectDisplayerInterface};
|
|
|
|
final class MyEntityDisplayer implements SubjectDisplayerInterface
|
|
{
|
|
public function supportsDisplay(Subject $subject, array $options = []): bool
|
|
{
|
|
return 'my_entity' === $subject->type;
|
|
}
|
|
|
|
public function display(Subject $subject, string $format = 'html', array $options = []): string
|
|
{
|
|
$label = sprintf('#%d %s', $subject->identifiers['id'], $subject->labels['name'] ?? '');
|
|
return 'html' === $format ? '<span>'.$label.'</span>' : $label; // <-- here, be aware that there is no xss risk. If any, use twig.
|
|
}
|
|
}
|
|
```
|
|
|
|
!!! warning
|
|
|
|
Use twig to render html format if there is a risk for xss injection.
|
|
|
|
## How it works internally
|
|
|
|
1. Storage: every audit is persisted as an `AuditTrail` entity.
|
|
2. Subject conversion: the main subject and all additional subjects are converted to `Subject` models using `SubjectConverterInterface` implementations, orchestrated by `SubjectConverterManagerInterface`.
|
|
3. Display: UI rendering uses `SubjectDisplayerInterface` implementations to render `Subject`s as HTML or strings.
|
|
4. Persistence: `TriggerAuditInterface` builds an `AuditEvent`, converts it to an `AuditTrail` via `AuditEvent2TrailInterface`, and persists it through the audit persistence layer (`AuditTrailPersisterInterface`, i.e. the persistence interface behind the service). There is no need to call `EntityManager::flush()` for the audit itself, so it is fast; however you must trigger the audit when the subject ids are available (see warning above).
|
|
|
|
See also:
|
|
- `Chill\MainBundle\Audit\TriggerAuditService`
|
|
- `Chill\MainBundle\Audit\AuditEvent2Trail`
|
|
- `Chill\MainBundle\Audit\AuditTrailPersister`
|
|
- `Chill\MainBundle\Audit\SubjectConverterManagerInterface`
|
|
- `Chill\PersonBundle\Audit\Displayer\HouseholdCompositionDisplayer`
|