mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-03 04:29:40 +00:00
Add audit documentation to developer guide
- 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.
This commit is contained in:
@@ -86,6 +86,7 @@ nav:
|
||||
- development/index.md
|
||||
- Access Control: development/access_control_model.md
|
||||
- API: development/api.md
|
||||
- Audit: development/audit.md
|
||||
- Assets: development/assets.md
|
||||
- Code Quality: development/code-quality.md
|
||||
- Create Bundle: development/create-a-new-bundle.md
|
||||
|
||||
212
docs/source/development/audit.md
Normal file
212
docs/source/development/audit.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user