Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Entity Audit Bundle Laravel Package

herdwatch/entity-audit-bundle

Herdwatch EntityAuditBundle adds automatic audit logging to Doctrine entities in Symfony apps. Track who changed what and when, store field-level diffs, and review a complete history of inserts, updates, and deletes without cluttering your domain code.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a maintained fork of simplethings/EntityAuditBundle — the API surface, workflow and commands are essentially the upstream ones. If you've used that bundle, nothing changes except the namespace and (possibly) the config root key.

As of v2.1.0 the fork merged upstream 1.23.0, which is the first substantive sync since the fork was cut. Practically that means the fork is tracking upstream again rather than drifting, so upstream docs and StackOverflow answers apply with more confidence than they did on 2.0.x. It also means config keys, service ids and command names were potentially touched by the merge — the release notes only say "Merge upstream 1.23.0" without enumerating changes, so verify against your installed version rather than trusting a blog post.

  1. Install:
composer require herdwatch/entity-audit-bundle
  1. Register the bundle (Flex does this automatically in config/bundles.php):
Herdwatch\EntityAudit\EntityAuditBundle::class => ['all' => true],
  1. Create config/packages/herdwatch_entity_audit.yaml. Because this is a low-adoption fork, confirm the exact root key and available options before writing anything custom — this matters more after the 2.1.0 upstream merge, not less:
bin/console config:dump-reference herdwatch_entity_audit
bin/console debug:container audit_reader   # service id may be herdwatch_entityaudit.reader / simplethings_entityaudit.reader
bin/console list audit                      # confirm command names survived the merge

A typical config:

herdwatch_entity_audit:
    table_suffix: '_audit'
    revision_table_name: 'revisions'
    global_ignore_columns: ['created_at']   # noisy columns you never want versioned
    revision_username: 'anon'
  1. Mark the entities you care about. With Doctrine attributes:
use Herdwatch\EntityAudit\Mapping\Annotation\Audited;

#[Audited]
class Product { /* ... */ }

(Use @Audited docblock annotations if you're still on annotation mapping — the fork keeps both. Upstream has been nudging toward attributes; a future merge may drop annotations without a major bump here, so prefer attributes in new code.)

  1. Create the audit tables:
bin/console audit:update
# or, if the fork prefixes commands:
bin/console herdwatch:audit:update

audit:update is a schema diff: it adds audit tables/columns for newly audited entities, it does not drop or alter existing ones.

First use case — "show me the history of product #42":

$reader = $container->get('audit_reader'); // AuditReaderInterface

$revisions = $reader->getRevisions(Product::class, 42);   // [1, 4, 9, ...]
foreach ($revisions as $rev) {
    $product = $reader->find(Product::class, 42, $rev);   // Product state at that revision
    $meta    = $reader->getRevision($rev);                // Revision: getTimestamp(), getUsername()
}

If that returns data, you're done with setup. Everything else is querying and reverting.

Implementation Patterns

Build a generic history endpoint. The most valuable day-to-day pattern is one controller that serves history for any audited entity:

#[Route('/history/{entity}/{id}', methods: ['GET'])]
public function history(AuditReaderInterface $reader, string $entity, int $id)
{
    $fqcn = $this->entityMap[$entity] ?? throw $this->createNotFoundException();

    return $this->json(array_map(fn (int $rev) => [
        'rev'       => $rev,
        'timestamp' => $reader->getRevision($rev)->getTimestamp()->format(DATE_ATOM),
        'username'  => $reader->getRevision($rev)->getUsername(),
    ], $reader->getRevisions($fqcn, $id)));
}

Diff two revisions. find() twice and compare with a normal Doctrine UnitOfWork-free get_object_vars/property loop. There is no built-in diff service; a small EntityComparator of your own is the idiomatic extension point.

"What changed in revision N?" is a single call and is the best hook for activity feeds:

$changed = $reader->findEntitiesChangedAtRevision($rev);
// returns entities hydrated at that revision; use instanceof / metadata to filter

Revert from the UI. The command is the safe path:

bin/console audit:revert Product 42 17 --dry-run
bin/console audit:revert Product 42 17

Programmatically, revert is: find() the entity at the target revision, then EntityManager::merge() and flush() — the merge re-audits, producing a new revision that restores the old state. That is a feature: reverts are themselves audited.

Capture the acting user. Point revision_username_callable (or override the Revision entity) at your token storage:

herdwatch_entity_audit:
    revision_username_callable: 'App\Audit\UsernameProvider::getUsername'

Audited associations. Add #[Audited] to owning-side collections to version the join table; you'll get <join_table>_audit with rev/revtype. To-one associations are stored as FK columns directly in the entity's audit row — no extra config needed, but the target entity must be audited too, otherwise the FK dangles.

Integration with tests. Audit writes happen at flush time; fixtures via doctrine:fixtures:load produce revisions 1..N. If you assert on revision numbers, pin them by resetting the schema in setUp() and using audit:update in your test bootstrap rather than doctrine:schema:update alone (which won't create audit tables).

Ignore the noise. global_ignore_columns for created_at/updated_at, #[Ignore] per property for things like lastSeenAt or denormalized counters. This is the single biggest lever on audit-table size.

Gotchas and Tips

  • Bulk operations bypass audit entirely. $em->createQuery('UPDATE ...'), NativeQuery, and DBAL statements never fire the listener. Either flush per entity or accept the gap explicitly.
  • audit:update never drops columns. Remove a property from an entity and the audit table keeps a NOT NULL column — inserts then fail. Every schema change needs a paired Doctrine migration for the audit tables (ALTER TABLE product_audit DROP ...). Make this a checklist item in review.
  • Custom Doctrine types must be registered on the audit connection too. The bundle copies column types verbatim; a json, uuid, or enum type that isn't registered in the audit entity manager throws at read time, not write time. Same for Embeddables — they're flattened and work, but nested types often don't.
  • revtype semantics: 1 = INSERT, 2 = UPDATE, 3 = DELETE. When you write raw queries against *_audit tables you need all three or you'll show resurrected rows for deleted entities.
  • Relations to deleted entities dangle. A ManyToOne FK in an old audit row points at an id that may no longer exist. Always load the related entity with find() and null-check; never assume the target is alive.
  • Don't use the main EntityManager for audit tables. $reader->getEntityManager() gives you the reader's connection/EM. Raw SQL against audit tables through the app EM will miss the audit schema and typically fails on the revisions join.
  • Revision numbers are per-transaction, not per-entity. Two entities flushed together share a rev. If you're paginating "changes for this entity", getRevisions() with $limit/$offset is the right tool; don't scan the revisions table.
  • Debugging a missing history row: check in order — (1) is the class annotated #[Audited], (2) was the entity loaded via find()/DQL or via a bulk/native path, (3) is the field in global_ignore_columns, (4) does the audit table actually have the row (SELECT * FROM product_audit WHERE id = 42). The listener is silent on skip; there is no warning.
  • Upgrading to 2.1.0 is a minor bump that isn't purely cosmetic. It merges upstream 1.23.0, so the changed code is largely not written by the fork maintainer — the fork's own test coverage of that merged code is unknown. Before deploying: pin the version in composer.json, run config:dump-reference, debug:container audit_reader and your revert/history smoke tests against a copy of production schema, and diff the published config tree against what you have in config/packages/. Config key, service id, and command-name changes are exactly the kind of thing an upstream merge can carry in silently.
  • Fork risk is lower than it was, not gone. The 2.1.0 merge shows the maintainer is actively syncing with upstream rather than letting the fork rot, which is the good news. The remaining risk: it's still a single-maintainer, near-zero-dependent package whose releases are driven by someone else's changelog. Treat every bump as "read the upstream changelog too", not just the fork's three-line release note — the useful detail lives upstream.
  • Extension points worth knowing: custom Revision entity (add ip, requestId, etc. — set revision_entity in config), custom AuditReader service decoration, and a postFlush listener of your own if you need to enrich revisions after the fact.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment