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

Audit Trail Bundle Laravel Package

rcsofttech/audit-trail-bundle

A Laravel package for tracking model changes and building audit trails. It records who changed what and when, supports configurable logging of create, update, and delete events, and helps teams review activity across their application.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is a Symfony bundle, not a Laravel package. There is no service provider, no config/audit-trail.php, and no Eloquent integration. If you are a Laravel developer, you have three realistic entry points:

  1. You maintain a Symfony service (or a mixed PHP estate) and need audit logging there.
  2. You are evaluating it as a reference implementation to port the pattern into owen-it/laravel-auditing or a custom observer/event pipeline.
  3. You are consuming audit records it produced, from a Laravel app via a shared database or HTTP endpoint.

Only option 1 is genuinely "day-to-day." Treat the package as a Symfony dependency and keep it out of your composer.json in Laravel-only projects.

Start with the changelog, not the README. The current major is v4.0.0 (CHANGELOG), and it arrived as a major bump, not a point release. Before you write a line of config, read the changelog for the 1.x → 4.x path. The release notes are the changelog — there is no other announcement channel — so treat that file as the primary documentation and re-read it on every install and every upgrade.

Minimal adoption (Symfony side):

composer require "rcsofttech/audit-trail-bundle:^4.0"

Enable the bundle in config/bundles.php:

return [
    // ...
    Rcsofttech\AuditTrailBundle\AuditTrailBundle::class => ['all' => true],
];

If that class does not autoload, stop and look in vendor/rcsofttech/audit-trail-bundle/ for the actual bundle class — majors are exactly when namespaces and entry-point classes move, and a wrong FQCN will produce a confusing container error that looks like a config problem. Confirm the name, then continue:

php bin/console config:dump-reference audit_trail
php bin/console debug:container --tag=audit_trail

Start with the Doctrine transport only — no HTTP, no queue. A minimal config/packages/audit_trail.yaml that records entity changes to a table is the entire value proposition, and it is the configuration you can debug. Add transports later.

First use case: pick one entity you already regret not auditing — a User, Invoice, or a permissions/role entity — register the bundle, perform an update through Doctrine, and confirm a row lands in the audit table. Do not start by auditing everything; the split-phase capture will generate rows for every flush in your fixtures and test suite, and your first impression will be a flood.

Where to look first in the repo: the CHANGELOG.md (for what v4 broke), the Doctrine subscriber/listener (what it hooks — onFlush vs postFlush), the transport interface, and the masking/normalizer layer. Those four files tell you more than the README.

Implementation Patterns

Automatic Doctrine capture (the default and the point). The bundle listens to Doctrine's unit-of-work and derives a change set from getEntityChangeSet(). In practice you configure an allow/deny list of entities rather than annotating every class:

audit_trail:
    doctrine:
        enabled: true
        entities:
            App\Entity\User: ~
            App\Entity\Invoice:
                exclude_fields: [updatedAt, version]

Keep the entity list explicit. Opt-in ages far better than opt-out, because every new entity you add is a decision rather than an accident. Verify the exact nesting against config:dump-reference audit_trail on the installed 4.x — key names are version-specific and this is the layer a major bump most often rearranges.

Split-phase is a feature, not an implementation detail. Capture happens during the flush (cheap, in-memory, has the change set); dispatch/serialization happens after commit. Exploit the gap:

  • Keep the capture phase free of I/O — no HTTP calls, no DB writes inside onFlush. If you extend the capture phase, assume you are holding the Doctrine transaction open and behave accordingly.
  • Put slow work (webhooks, Slack, S3 archival) behind the queue transport, so a commit is never blocked by a third-party timeout.
  • If a request rolls back, you should still expect a captured-then-discarded event, not a persisted one. Test rollback explicitly; it is the number one source of "why is there no audit row?" confusion.

Transport selection by environment. This is where the bundle pays off compared to a single-writer library:

when@dev:
    audit_trail:
        transport: doctrine   # inspect rows directly in your DB client

when@prod:
    audit_trail:
        transport: queue      # async, resilient, backpressure-tolerant
        queue:
            name: audit_trail
            retry: 3

Wire the HTTP transport only when audit records belong to a separate compliance service with its own retention. That is a real requirement in regulated environments and the bundle's main differentiator; using it for in-app logging is overkill.

Upgrade the config as a migration, not a merge. Commit config/packages/audit_trail.yaml, then after every major bump re-dump the reference and diff it against your file. On v4 specifically, assume at least one renamed or relocated key until the dump proves otherwise. A silently ignored key means silently missing audit rows.

Sensitive data masking — configure, don't hand-roll. Define a mask list once and treat it as a security control with tests:

audit_trail:
    masking:
        fields: [password, rememberToken, apiKey, iban, nationalId]
        strategy: hash   # or redact / partial

Add a test that creates a user with a known password and asserts the string does not appear anywhere in the serialized audit payload. Also assert it against the diffs array, not just the entity snapshot — masking layers often miss one path, and a major-version rewrite of the normalizer is exactly where that regression would land.

Integration tip: read-side. The bundle is a writer. Your admin UI still needs a query layer:

$history = $auditRepository->findBy(
    ['entityClass' => User::class, 'entityId' => (string) $user->getId()],
    ['createdAt' => 'DESC'],
    50,
);

Index (entity_class, entity_id, created_at) on day one. Retrofitting an index onto a large audit table on a live system is a maintenance window you do not want.

Gotchas and Tips

No Laravel integration exists, and none appears in the release line. Dependents are negligible, it is a one-maintainer project, and v4 brings no Laravel story. Porting the idea is cheap (an Eloquent observer plus a queued job); porting the bundle's class graph is not. If you are on Laravel, budget half a day to reimplement the change-set plus masking pattern and move on. Do not shim Symfony's container into a Laravel app for this.

v4.0.0 is a major version — read the diff before you upgrade, not after. The changelog link is the only source of truth for what broke. Two things to check explicitly: the config tree (key names, nesting, transport options) and the stored payload/diff shape. Either one changing will break your read-side code quietly, with no error — you will just render empty history panels. Pin "rcsofttech/audit-trail-bundle": "^4.0" for production, and pin tighter (4.0.*) during initial evaluation so a 4.1 cannot move under you mid-prototype.

Doctrine coupling is total. The capture phase depends on the unit of work. Anything that bypasses the ORM — DB::table() equivalents, raw SQL, bulk DQL updates — will produce no audit records. Audit completeness is a claim about the code path, not the database. Document which write paths are unaudited, and check for QueryBuilder::update() calls that silently skip the listener.

Split-phase ordering bites during flush. If other Doctrine listeners write entities in postFlush (and many do), whether those writes are captured depends on registration order. Symptom: audit rows for entity A but not the related entity B written in the same request. Diagnosis path is debug:event-dispatcher on the Doctrine events, plus checking whether the second write happens outside a flush entirely.

Queue transport and serialization. Queued payloads must survive the boundary from in-memory to serialized. Two failure modes:

  • Entities or Proxies captured in the payload fail to unserialize in the worker. Ensure the serializer sees arrays, not objects — and re-verify this after a major bump, since payload construction is precisely what a rewrite touches.
  • The worker is not running. Audit "goes missing" in staging because messenger:consume was never started — a classic. Add a health check or a canary test that asserts one audit row arrives end-to-end per deploy.

Config quirks to verify, not assume. The bundle's own keys are the source of truth; config:dump-reference audit_trail is your only reliable documentation for option names and nesting. Never copy a config snippet — including the ones above — from a blog post or an older assessment without confirming it against the dump of the version you actually installed.

Performance sanity check before rollout. Audit logging sits on the hot write path. Load-test one endpoint that flushes 200 entities and confirm the capture phase does not balloon query count. If it does, tighten the entity list and field exclusions before touching transports — the transports are almost never the bottleneck.

Extension points worth knowing. The transport interface and the masking/normalizer hooks are where customization belongs. Prefer a custom transport over forking the listener; a fork guarantees you cannot take the next release, and this package now has a demonstrated appetite for majors — you will need that upgrade path.

Overall verdict for a Laravel shop: the architecture is genuinely worth stealing (split-phase capture plus pluggable transports), direct applicability to Laravel remains zero. The previous risk profile — "too young, effectively one release" — no longer holds; reaching v4 means the package is actively iterated. The risk has shifted rather than disappeared: at a major-version-every-few-months cadence, with one maintainer and a changelog as the only documentation, the thing that will hurt you is churn, not abandonment. Prototype with it, pin hard, read every changelog, and until you have personally validated v4's payload shape, do not bet a SOC 2 audit on it.

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.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify