wedevelopnl / audit-log
Immutable, self-contained audit trail for Symfony and Doctrine.
Package info
github.com/wedevelopnl/audit-log
Type:symfony-bundle
pkg:composer/wedevelopnl/audit-log
Requires
- php: >=8.5
- doctrine/dbal: ^4.0
- doctrine/doctrine-bundle: ^3.2
- doctrine/orm: ^3.6
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/clock: 8.0.*
- symfony/config: 8.0.*
- symfony/dependency-injection: 8.0.*
- symfony/framework-bundle: 8.0.*
- symfony/http-foundation: 8.0.*
- symfony/http-kernel: 8.0.*
- symfony/security-core: 8.0.*
- symfony/translation-contracts: ^3.5
- symfony/uid: 8.0.*
Requires (Dev)
- deptrac/deptrac: ^4.6
- friendsofphp/php-cs-fixer: ^3.95
- igor-php/igor-php: ^0.6
- infection/infection: ^0.33
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^13.1
- rector/rector: ^2.4
- symfony/cache: ^7.2
- symfony/security-bundle: 8.0.*
- tomasvotruba/type-coverage: ^2.2
This package is auto-updated.
Last update: 2026-06-01 07:14:54 UTC
README
Immutable, self-contained audit trail for Symfony and Doctrine.
An audit record answers — durably and credibly — who did what, to what, when, from where, and what changed, and keeps answering it regardless of what later happens to the rest of the system. Records are immutable snapshots of a past fact: they survive deletion of the actor, the subject, and the producing code.
The package ships as a Symfony bundle: a framework-free core (value objects, the
AuditEvent contract, the Recorder and read ports) plus Doctrine persistence and
Symfony runtime adapters, pre-wired by the bundle. The core stays free of Symfony and
Doctrine (ADR-0009).
Requirements
- PHP 8.5+
- Symfony 8.0 (
framework-bundle,security-core,translation, and the components pulled in transitively) - Doctrine (
dbal^4,orm^3.6,doctrine-bundle^3.2)
Installation
composer require wedevelopnl/audit-log
There is no Flex recipe; register the bundle manually in config/bundles.php:
return [ // ... WeDevelop\AuditLog\AuditLogBundle::class => ['all' => true], ];
The bundle auto-registers its Doctrine DBAL types and the AuditRecordEntity mapping.
See docs/installation.md for the migration, the recommended
append-only database grant, and the services you must provide.
Design
Three responsibilities are deliberately separated:
| Responsibility | Type | Concern |
|---|---|---|
| Describe an act | AuditEvent |
Pure data + phrasing. No clock, identity, or services. |
| Capture the moment | Recorder |
Resolves time, actor, origin, subject label; freezes everything. |
| The durable read shape | AuditRecord |
Immutable snapshot, interpretable without the producing code. |
| Query the trail | RecordReader |
Paginated, filterable reads (AuditQuery → AuditPage) for display. |
The architecture derives from six governing properties — immutable, self-contained, faithful to the moment, attributable, intelligible, queryable. The rationale lives in the Architecture Decision Records.
Usage
Describe an auditable act by implementing AuditEvent (or extending
AbstractAuditEvent for sane defaults):
use WeDevelop\AuditLog\Event\AbstractAuditEvent; use WeDevelop\AuditLog\Event\Changeset; use WeDevelop\AuditLog\Event\FieldChange; use WeDevelop\AuditLog\Event\Subject; final readonly class UserRoleChanged extends AbstractAuditEvent { public function __construct( private string $userId, private string $from, private string $to, ) { } public function code(): string { return 'user.role_changed'; } public function subject(): Subject { return new Subject(User::class, $this->userId); } public function changes(): Changeset { return new Changeset(FieldChange::of('role', $this->from, $this->to)); } protected function parameters(): array { return ['from' => $this->from, 'to' => $this->to]; } }
Record it through the Recorder — the single write seam, autowired by the bundle.
It captures the ambient strands of the moment (clock, acting principal, origin,
subject label) and appends a frozen record:
$recorder->record(new UserRoleChanged($userId, 'member', 'admin'));
To attach human-readable subject labels, provide a SubjectLabeller (the default
returns none); read the trail back through RecordReader. See
docs/installation.md for wiring.
Sensitive fields are recorded as changed without their values:
new Changeset(FieldChange::redacted('password'));
License
BSD 3-Clause. See LICENSE. © 2026 WeDevelop.