xiidea/easy-audit
Easy Audit adds simple, automatic audit trails to Laravel Eloquent models. It logs create, update, and delete events with old/new values, records the responsible user, and offers configurable tracking for reliable change history.
First, the honest framing: xiidea/EasyAuditBundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/easy_audit.php, no facade. It hooks into Doctrine ORM lifecycle events to record entity changes (who changed what, when) into an audit table. If your Laravel app is Eloquent-first, this package is not a drop-in — the Laravel-native equivalent is owen-it/laravel-auditing. Reach for EasyAuditBundle only when you are already running Doctrine entities in Laravel (via laravel-doctrine/orm) or when the Laravel app sits alongside a Symfony service that shares the database.
Minimal steps (Symfony-style host):
composer require xiidea/easy-audit-bundle — confirm the exact Packagist name against the README before adding it to composer.json; the repo name EasyAuditBundle and the package name do not always match.config/bundles.php (Symfony Flex usually does this for you).easy_audit config block (entity manager to listen on, audit log class, table prefix).#[Auditable] attribute (or the legacy annotation on older codebases).Where to look first: the README's configuration reference, then the Auditable attribute class, then the audit log entity. Those three tell you 90% of the behaviour. Ignore the revision/timeline helpers until you have one row landing correctly.
First use case: audit a single high-value entity (invoice, subscription, payout) and render a "history" tab in your admin UI. Do not start by auditing every entity — you will drown in rows and write amplification before you have proven the read path.
Attribute-per-entity, opt-in. Auditing is enabled by annotation/attribute on the entity class, not globally. Keep the auditable surface small and intentional; every audited entity adds a write on each flush and a row per change.
Change-only capture. The bundle records the diff — changed fields only — which makes the audit table readable and keeps it small. Design your UI around "field X: old → new" rather than full snapshots. If you need full snapshots, archive them yourself; this is not a versioning/rollback store.
Blame / user attribution. User identity comes from the security token storage. That works in HTTP requests and breaks in every other context (queues, scheduler, console commands, seeders). Pattern: wrap job execution in a small helper that pushes an explicit user id into the token storage (or a stub TokenInterface) before the flush, and pops it after. Document that "system" is a legitimate actor value.
Custom, non-entity audit entries. For events that are not a Doctrine write (login, export, permission change, API key rotation), use the bundle's audit logger service directly to append a manual entry. This gives you one uniform timeline table instead of two audit systems.
Reading history. Query the audit log repository by (class, id) ordered by revision/timestamp. Build two read paths early: a per-record timeline (admin detail page) and a per-actor feed (compliance/"what did this admin do"). Index accordingly — (object_class, object_id, created_at) and (user_id, created_at).
Split the write path. If your audit volume is high, point the bundle at a second entity manager/connection so audit inserts do not compete with your transactional writes. Slight consistency tradeoff — decide deliberately.
Retention as a scheduled job. Audit tables grow monotonically. Ship a pruning/archival command on day one (e.g. keep 24 months hot, move the rest to cold storage). Retrofitting this after 50M rows is painful.
Testing. Boot the kernel with a test database, persist → flush → assert an audit row exists with the expected changed fields. Explicitly test the "no-op update" case (flush with no changes) and assert zero rows — that is the most common false expectation.
In a Laravel app via Doctrine: register the bundle's services manually or through a small bridge service provider, map the config array from config/easy_audit.php, and ensure the Doctrine event subscribers are attached to the manager Laravel's Doctrine integration builds. This is real work — budget a spike before committing.
The Symfony 7.4 change in 3.0.2. This is a dependency-constraint widening (symfony/* now allowed up to 7.4), not a behaviour change. Practical impact: composer update will now resolve Symfony 7.4 where it previously refused. Low risk, but re-run your audit tests — the underlying risk is Doctrine/Symfony event-dispatcher behaviour shifting under you, not this bundle's own code. If you pin Symfony tightly for other bundles, verify the whole set resolves together before merging.
Bulk operations silently skip auditing. DQL UPDATE/DELETE, $qb->update(), raw SQL, and ->where(...)->delete() on a query builder bypass entity lifecycle events entirely, so no audit rows are written. This is the number one compliance hole in Doctrine-based auditing. Enforce a rule: state-changing bulk queries on audited entities are forbidden, or must be paired with a manual audit entry.
Collections and relations are shallow. Changes to the owning side's scalar columns are recorded; adds/removes inside a OneToMany collection often are not, and nested/embedded changes can be lossy. Treat the audit trail as "field-level on this entity," not "graph-level." Verify with a real association test before promising relationship history.
Do not mutate audited entities inside listeners. The audit subscribers run during flush. Write to your audit log only; touching the managed entity or triggering another flush in the same listener leads to recursion, EntityManager is closed, or partially recorded revisions.
Uninitialized proxies and lazy loading. Computing changesets can force-load lazy associations and produce extra queries (or LazyLoadingException outside a transaction). Keep audited entities free of heavy lazy relations, or configure the mapping so the diff path never needs them.
Flush ordering surprises. If one request updates five entities in a single flush, you get a batch of audit rows — but the revision ordering across them is not guaranteed to match your business intent. If order matters, flush per aggregate.
user_id is null more often than you think. Queue workers, console commands, and impersonation all produce null or wrong blame. Add a non-nullable actor_type/actor_label with an explicit "system" fallback and alert on unexpected nulls rather than discovering it during an audit.
Config and cache. In production, config changes require a cache clear. A stale container is a classic cause of "auditing stopped after deploy" — the subscriber wasn't re-registered.
Soft deletes and hard deletes. A soft delete is just an update (fine). A hard delete writes a delete revision with the last known state at best — if you need full "what did the row look like before it vanished," capture it yourself at delete time.
Version pinning. Since 3.0.2 is a patch, ^3.0 is the sane constraint. Treat any future 3.x minor as "read the changelog" because the Symfony constraint and the Doctrine event API have both been moving targets.
Extension points worth knowing. The audit log entity class and the audit manager/logger service are the two seams you will actually subclass or decorate: swap the entity to add columns (request IP, correlation id, tenant id), and decorate the logger to enrich entries without forking the bundle. Everything else is best left alone.
How can I help you explore Laravel packages today?