EntityAuditBundle: a mirror table for every audited Doctrine entity
Audit for Doctrine Entities
At a glance
- What is it?
- A Symfony bundle that versions Doctrine 2 entities and their associations by writing each change to a mirrored audit table, inspired by Hibernate Envers. It hooks into schema generation so the DDL appears on its own, and it comes with an AuditReader for asking what an entity looked like at a given revision.
- Who is it for?
- Adopt EntityAuditBundle if you run Doctrine 2 inside Symfony and need to reconstruct the state of an entity at an earlier point in time, including the state of its associations, without building that yourself. Skip it if you only need a change log of one field, since a timestamp column or an application level log is far simpler.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 173 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The mechanism is a mirror table per audited entity
There are a bunch of different approaches to auditing or versioning of database tables, and this extension takes the mirroring one. It creates a mirroring table for each audited entity's table that is suffixed with "_audit".
Besides all the columns of the audited entity, the mirror table has two additional fields. The first is rev, which contains the global revision number generated from a "revisions" table. The second is revtype, which contains one of 'INS', 'UPD' or 'DEL' as information on which type of database operation caused the revision log entry.
The global revision table is the index across all of that. It contains an id, a timestamp, a username and a change comment field. So every audited entity's audit table rows point at a row in revisions, and that row carries who made the change and when, plus a free text comment.
The two extra columns are worth dwelling on for a moment, because they are the whole interface. Because rev is a global number rather than a per-entity sequence, a single query can ask for everything that happened anywhere in the system at revision 412, which is what makes cross-entity history possible rather than just per-entity history.
The claim that follows from this is the one in the readme: with this approach it is possible to version an application with its changes to associations at the particular points in time. The association is the hard part, and that is where the mirror table earns its keep.
The licence note in the readme says this package is available under the LGPL license, with a link to the LICENSE file, while the repository's own licence field records MIT. If the licence matters to you, read the LICENSE file in the repository rather than trusting either label, since the two disagree.
The schema tool creates the audit tables for you
The second design decision is that you do not write the audit DDL yourself. This extension hooks into the SchemaTool generation process so that it will automatically create the necessary DDL statements for your audited entities.
That is a bigger convenience than it first appears. The audit tables mirror the audited tables, so they have to stay in step with them. If you add a column to an audited entity, the mirror table has to gain it too, and if the two drift apart the audit history becomes unusable for that field. Wiring the creation into the schema tool means the mirror follows the entity automatically.
The flip side is that the audit tables appear in your schema diff, and a developer who does not know why there is a second table next to each audited one will eventually run a migration that drops it. That is a documentation problem rather than a design problem, and it is worth putting a line in your contributor guide about it.
The readme gives you a way to see what would be created before you commit to it:
./bin/console doctrine:schema:update --dump-sqlThat command shows the new tables in the update schema queue, which is exactly the check to run after adding an entity to the audited list.
The auditing itself is driven by Doctrine event listeners, since the readme's standalone section says you have to pass the entity class names to be audited to the MetadataFactory instance and configure the two event listeners. The standalone path builds an AuditConfiguration, sets the audited entity classes, creates an AuditManager and registers the events on a Doctrine EventManager before the EntityManager is created with that event manager passed in.
Installing it in a Symfony application
The install is a Composer require, a bundle registration and a configuration file, which is the standard shape for a Sonata bundle.
$ composer require sonata-project/entity-audit-bundleThen the bundle is enabled in the kernel:
// config/bundles.php
return [
//...
SimpleThings\EntityAudit\SimpleThingsEntityAuditBundle::class => ['all' => true],
//...
];Note the namespace. The bundle is published as sonata-project/entity-audit-bundle but the PHP namespace is SimpleThings\EntityAudit, which is a leftover from its origin and is worth knowing before you search for configuration or a class name.
The configuration is a single extension, simple_things_entity_audit, and the first thing you set is which entities are audited:
# config/packages/entity_audit.yaml
simple_things_entity_audit:
audited_entities:
- MyBundle\Entity\MyEntity
- MyBundle\Entity\MyEntity2There are three more settings worth knowing about. global_ignore_columns excludes properties from triggering a revision, which is what you want for a created_at or updated_at column that changes on every write:
simple_things_entity_audit:
global_ignore_columns:
- created_at
- updated_atThe connection and entity_manager settings exist to work with something other than the default:
simple_things_entity_audit:
connection: custom
entity_manager: customAnd disable_foreign_keys explicitly discards the foreign keys that would otherwise be inferred from the audited entities, which is the setting to reach for if the inferred keys cause trouble in your schema.
AuditReader, and the caveat about detached instances
Reading the audit information is done with a SimpleThings\EntityAudit\AuditReader instance, and in Symfony it is injected into the controller as a service argument:
use SimpleThings\EntityAudit\AuditReader;
class DefaultController extends Controller
{
public function indexAction(AuditReader $auditReader)
{
}
}In a standalone application you create the reader from the audit manager, with $auditReader = $auditManager->createAuditReader($entityManager).
The find method returns the state of the entity at the given revision, even if the last change to that entity was made in a revision before the given one. That sentence is the interesting one, because it describes a lookup into history rather than a replay: you ask for revision 10 and get the entity as it was then, whether or not revision 10 touched it.
The caveat is documented and it will bite you if you miss it. Instances created through AuditReader#find() are NOT injected into the EntityManagers UnitOfWork, they need to be merged into the EntityManager if it should be reattached to the persistence context in that old version.
So a reconstructed entity comes back detached, and if you then modify and save it, the change may not go where you expect unless you merge it first. The phrasing about that old version suggests this was written against an earlier Doctrine behaviour and has not been revisited in the text, which is one of the reasons to read the current code rather than trust the readme text on this point.
Where the mirroring approach stops being the right one
There are several cases where this bundle is the wrong tool, and they are worth naming because the mirroring model has real limits.
The first is when you need the audit rows to be queried as the source of truth. A mirror table per entity is a copy of the data, and a copy is only as good as the process that maintains it. If your requirement is an append-only event log with a schema you control, or a stream that other systems consume, you want a different mechanism.
The second is volume. Every write to an audited entity produces a second row, so the storage cost of the audit tables grows with the number of writes rather than with the size of the current data, and the tables are never pruned by anything in the bundle. An application with frequent updates to large entities will accumulate history quickly, and you have to decide yourself what to keep.
The third is the scope of what gets recorded. The bundle records the entity and its associations, which is its strength, but it records them as rows, not as a description of what changed. If you want to know which field changed between two revisions, the mirror table has the two versions and you compute the difference yourself.
The fourth is the branch situation. The default branch is 1.x, there is a separate 2.x line, and the CI table in the readme tracks a test workflow and a coverage status for each. The 1.x line has release 1.23.1 from 2026-03-24 and 1.23.0 from 2025-12-01, and the 1.22.0 from 2025-10-18 is the third most recent. Two maintained lines with different Doctrine and Symfony support matrices is a fact to check before you pick one, because choosing the wrong one costs you an upgrade later.
And the fifth is a general one. A global revision counter is a global lock on ordering, and the readme does not describe how it is generated or how concurrent transactions behave against it. For a high write throughput system, that is the first thing to test.
Against Hibernate Envers, and against an application level log
Two comparisons make the trade clear.
The first is against Hibernate Envers, which the readme names as the inspiration. The difference in approach is the ecosystem rather than the idea. Envers lives inside the Hibernate ORM, so it hooks into an event system that already exists and its audit tables are created as part of the schema tooling there. This bundle reaches the same behaviour from outside, by attaching its own listeners to Doctrine's event manager and by hooking into Doctrine's SchemaTool. The consequence for you is that the Envers approach is more integrated and this one is more portable across ORMs, at the price of more configuration and a namespace that does not match its package name.
The second is against writing an application level log. That approach is almost free: a listener writes a row with the entity id, the changed fields and the current user into a table you designed yourself. The difference is in what you get back. A hand written log records the changes; a mirror table records the state, so you can ask what the entity looked like at revision 10 without reconstructing it, and you get the associations at that point in time rather than a description of them.
So the deciding question is whether you need state reconstruction or a change record. If you need to know what a record looked like at a past revision, with its relationships, the mirroring model is doing something the change log cannot, and the extra storage and the extra configuration are the price. If you only want to know what changed and when, a log table is smaller, easier to query, and you control its shape.
Editorial conclusion
Adopt EntityAuditBundle if you run Doctrine 2 inside Symfony and need to reconstruct the state of an entity at an earlier point in time, including the state of its associations, without building that yourself. Skip it if you only need a change log of one field, since a timestamp column or an application level log is far simpler. Verify first which branch you are on, because the default branch is 1.x while a 2.x line exists separately, and check that your Doctrine and Symfony versions are in range for the release you install, since the audit tables are created by the schema tool and a mismatch shows up as a migration problem rather than a runtime one.
Frequently asked questions
How does the EntityAuditBundle store the audit history?
It creates a mirroring table for each audited entity's table, suffixed with _audit, holding all the entity's columns plus a rev field with the global revision number from a revisions table and a revtype field containing 'INS', 'UPD' or 'DEL'. The revisions table holds an id, a timestamp, a username and a change comment field.
How do I install and enable the EntityAuditBundle?
Run composer require sonata-project/entity-audit-bundle, register SimpleThings\EntityAudit\SimpleThingsEntityAuditBundle in config/bundles.php, and create config/packages/entity_audit.yaml with the simple_things_entity_audit extension listing the audited entities.
How do I find the state of an entity at an earlier revision?
Use AuditReader#find() with the entity class, the id and the revision number. It returns the state of the entity at that revision, even if the last change to that entity was made in an earlier revision.
Why do the audit tables not appear in my database?
The extension hooks into the SchemaTool generation process so it creates the DDL for the audit tables automatically, but you still have to apply it. Running ./bin/console doctrine:schema:update --dump-sql shows the new tables in the update schema queue before you apply the migration.
Can I exclude some columns from being audited?
Yes, through the global_ignore_columns setting, which takes a list of property names that should not trigger a revision. The readme's example excludes created_at and updated_at.
What licence does the EntityAuditBundle use?
The readme says the package is available under the LGPL license and links the LICENSE file, while the repository's own licence field records MIT. The repository is not archived, the last push was on 2026-05-03, and recent releases include 1.23.1 on 2026-03-24 and 1.23.0 on 2025-12-01.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sonata-project-entityauditbundle)Was this useful?