nti/audit-bundle
nti/audit-bundle provides audit trail functionality for Symfony applications, automatically logging Doctrine entity changes such as creates, updates, and deletes with configurable storage and user attribution for reliable change history.
This bundle is a fork of the original data-dog/doctrine-audit-bundle, so most public docs and examples from DataDog apply directly. It targets Symfony 3 + Doctrine ORM — not Symfony 4/5/6, not Flex.
Install the dependency:
composer require nti/audit-bundle
Register the bundle in app/AppKernel.php (Symfony 3, no autowiring):
$bundles = [
// ...
new Nti\AuditBundle\NtiAuditBundle(),
];
Configure the base audit entity in app/config/config.yml and point the bundle at an entity that extends AbstractAuditLog (or use the provided one if your DB permits):
nti_audit:
audit_log_class: AppBundle\Entity\AuditLog
Create/update the audit tables. The audit log tables are normal Doctrine entities, so doctrine:schema:update --force (or a migration) creates them. Expect extra tables: one per audited entity plus a shared audit_log table.
First use case — audit a single entity. Annotate a model with @Audit (and @AuditReader on the audit entity if you use a custom one):
use Nti\AuditBundle\Annotation\Audit;
/**
* @ORM\Entity
* @Audit
*/
class Invoice { /* ... */ }
Insert/update/delete a row, then inspect the generated *_audit table — it should contain a revision entry with serialized old/new values.
Where to look first in the repo: the EventListener/ and Doctrine/ directories (the onFlush/preUpdate listeners), Annotation/Audit.php, and Resources/config/ for the DI wiring. The Command/ directory is worth a look too — it holds the log-retention/purge command you'll want once audit tables start growing. The DataDog docs (README, docs/) are the de facto reference.
Per-entity audit entities. For each audited entity you typically create a companion audit entity extending AbstractAuditLogEntity and register it via @AuditReader. This gives you typed columns for the diff and a clean way to query history.
Blame / user attribution. Wire the bundle's blame listener to your security token storage (or set a custom blame_user resolver service). Every revision row then records who made the change — the single most requested audit requirement.
Reading history in a controller. Query the audit repository by targetEntity + objectId, ordered by revision/loggedAt, and render a timeline view. The bundle stores a JSON/array diff per revision (oldValues, newValues), so a simple "field A: X → Y" table is a few lines of Twig.
Retention / pruning. The bundle ships a command that deletes audit logs older than a given age. Schedule it (cron or a Symfony command scheduler) against your audit_log table early — audit tables grow monotonically and a busy entity can outgrow its source table within months. If you're on v3.0.1 or older, bump to v3.0.2 first: that patch fixes class-name resolution in the delete-old-logs command, so the purge silently targeted the wrong class before. Verify the command actually removes rows in staging before turning it loose on production history.
Ignoring noisy fields. Annotate transient fields (updatedAt, cache counters, tokens) with the ignore option so you don't generate false-positive revisions. This is critical for entities that touch updatedAt on every save.
Integration with workflows. For state-machine style transitions (order status, ticket lifecycle), audit logs become the transition ledger — subscribe to doctrine.event_subscriber postFlush and mirror key revisions to a message bus or event dispatcher so downstream consumers see "what changed".
Relations. The bundle records collection changes as AuditEntry / relation deltas rather than full object graphs. If you need "which parent did this child move under", log the foreign key explicitly in your diff view — don't assume the collection diff is enough.
Testing. Use an in-memory SQLite DB, run a couple of persists/flushes, and assert revisions exist. The listeners run on onFlush so your functional test must go through the real EntityManager — mock repositories won't emit events.
doctrine-audit, auditor-bundle, gedmo/loggable) are usually better fits.onFlush ordering matters. The listener inspects the UnitOfWork's scheduled inserts/updates/deletions; if another listener alters the UoW (e.g. soft-delete, versioning) before it runs, the diff is wrong. Register it last, or move reads to postFlush when you depend on generated IDs.schema:update silently skips them if the audit entity isn't mapped — always diff your schema after enabling a new @Audit entity.@Audit to AuditLog while copying annotations; guard against it or you'll create infinite revisions.< 5.7) and SQLite store the diff as text in a TEXT/LONGTEXT column. Check the charset/collation — non-UTF8 field values (binaries, encrypted blobs, legacy latin1 columns) will corrupt or truncate the log.flush flag or detach the listener) — otherwise a 100k-row import becomes a multi-minute transaction.doctrine/orm range is narrow; pinning newer doctrine/dbal (e.g. v3) will break the UoW API this bundle relies on. Treat composer.lock as sacred.services.yml aliasing rather than editing vendor — those three are the seams the bundle was designed to flex.getScheduledEntityUpdates() at onFlush time, and (b) whether the changed field is in the ignore list. The listener swallows many "nothing to record" cases silently.How can I help you explore Laravel packages today?