zhortein/auditable-bundle
An auditing package for Laravel applications that automatically tracks model changes. It logs create, update, and delete events with changed attributes, original values, user IDs, and timestamps, making it easy to see who changed what and when. Simple configuration and Eloquent integration.
Start with UPGRADE-2.0.md, not the README. The headline fact of 2.0.0 is that nothing changes unless you opt in: without new configuration you keep the legacy AuditEntry mapping, the audit_entry table, integer identifiers, the automatic listener, and fail-open behaviour. That means an existing install can take the version bump with no migration and no code changes — verify that first before reading anything else.
Then answer one question: is your app staying legacy, or moving to strict transactional audit mode? Everything downstream depends on it.
Minimum viable first use case for the strict mode:
new DateTimeImmutable().If you are on Laravel rather than Symfony, stop here: 2.0.0 requires Doctrine ORM 3, DoctrineBundle 2.19/3.x, Symfony 7.4/8.x and Symfony Security for actor resolution. There is no Eloquent path and no Laravel Security integration in this release.
Wrap the audited operation, don't audit inside the write. The bundle deliberately performs no hidden flush and no transaction control. The idiomatic pattern is a service method that opens the transaction, performs the domain write, records the audit, and commits once:
public function rename(Product $p, string $name): void
{
$this->em->wrapInTransaction(function () use ($p, $name) {
$p->rename($name);
$this->recorder->record($p); // no flush here
});
}
That is what makes the PostgreSQL atomicity proofs meaningful: either both the domain row and the audit row land, or neither does. Fight the temptation to call flush() inside your storage adapter — you will get interleaved flushes and partial commits, which is exactly the failure mode strict mode exists to prevent.
Treat fail-closed as a product decision, not a config flag. In strict mode a broken audit storage or a bad factory takes down the user-facing operation. Before enabling it, decide which flows may fail and confirm your error handling turns an audit failure into a clean 5xx rather than a half-applied write.
Model your audit entity for how you will query it. Because the entity is application-owned, you choose the shape: actor identifier, impersonator identifier, subject class plus identifier, action verb, before/after payload, timestamp. Add indexes on (subject_class, subject_id, occurred_at) from day one — you will be reading "history of this record" far more than you write.
Use deterministic identifiers. 2.0.0 supports UUIDs and primitive composites, not just integers. This matters when your domain uses UUID keys: the audit row can reference the subject directly without a lookup table. Keep the identifier representation in the factory and storage identical, or queries will silently miss rows.
Lean on actor + impersonation context. Symfony Security resolution gives you both the acting user and the impersonator. Surface both in the audit entity from the start; retrofitting "who was really doing this" after an incident is not possible.
Test against PostgreSQL, in transactions. Build a small suite that asserts three things per happy path: commit persists both rows, an exception rolls back both, and a storage failure in strict mode aborts the domain write. SQLite will pass locally and tell you nothing about the semantics that matter.
Keep a compatibility shim while migrating. Legacy and strict modes can coexist during a transition: leave legacy mapping on, introduce your own entity and recorder for new flows, and backfill only when you are ready to make the cut. There is no mandatory migration for staying legacy, so this can be a slow, boring rollout.
composer why doctrine/orm before bumping.DateTimeImmutable anywhere in the audit path is a test-smell that will eventually cost you a debugging day.How can I help you explore Laravel packages today?