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

Loggable Bundle Laravel Package

ibrows/loggable-bundle

Symfony bundle that automatically logs changes to Doctrine entities, tracking who created, updated, or deleted records. It hooks into Doctrine listeners and the Symfony event system to build an auditable history of your application's data without repetitive logging code in every service.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before anything else. ibrows/loggable-bundle is a Symfony bundle, not a Laravel package. It wires itself into Doctrine ORM's unit of work via event listeners (onFlush / postFlush-style) and into Symfony's DI container. There is no ServiceProvider, no Eloquent integration, and no artifact you can composer require into a Laravel app that "just works." If your stack is Laravel, treat this repo as a reference implementation you read once and reimplement in ~100–300 lines of Eloquent observer code. That is genuinely a 1–2 day job and removes all dependency risk (0 stars, 0 dependents, no maintenance signal).

Minimal steps (Symfony + Doctrine project):

  1. composer require ibrows/loggable-bundle
  2. Register in config/bundles.php:
    return [
        // ...
        Ibrows\LoggableBundle\IbrowsLoggableBundle::class => ['all' => true],
    ];
    
  3. Ensure the log entity's schema exists — inspect the bundle's mapped entity, then either doctrine:schema:update --dump-sql or generate a migration so the *_log table is versioned.
  4. Mark one entity loggable using the bundle's annotation/attribute (class-level vs. property-level — confirm from the annotation class).
  5. Smoke test: persist → modify a field → flush, then query the log table and inspect the diff payload.

Where to look first (three files, everything else is commentary):

  • Resources/config/services.* — which Doctrine events are subscribed and which service IDs do the work.
  • The Loggable annotation/attribute class — whether you opt in per-entity or per-property, and whether an "ignore" variant exists for volatile fields (last_login, updated_at, counters).
  • Entity/LogEntry*.php (or similarly named) — the storage contract: action, object class, object ID, version, diff payload, actor, timestamp. You will query this shape forever, so read it before writing any admin UI.

First use case — pick something boring but high-stakes. Annotate an Invoice, Subscription, or PriceRecord entity, not User. Logging a hot, frequently-mutated entity on day one floods the table and teaches you nothing except how to delete rows. Start where a support person would actually ask "who changed this and from what to what?"

Laravel translation table (what you actually build):

Bundle concept Laravel equivalent
Doctrine onFlush listener Model::updated() observer / Eloquent\Events
Annotation/attribute opt-in A Loggable trait or $logAttributes allow-list on the model
Log entity + table Eloquent model AuditLog + migration
Actor resolution from Symfony security context auth()->id() inside the observer
Diff payload (old/new per field) $model->getOriginal() vs $model->getChanges() in updated
Delete tracking deleting observer capturing attributes before delete

If you're on Laravel and want a maintained package instead of a port, spatie/laravel-activitylog (broad activity log) or owen-it/laravel-auditing (field-level before/after) are the established choices — evaluate both before deciding to hand-roll.

Implementation Patterns

1. Opt-in narrowly, then widen. Start with the attribute on a single entity. As confidence grows, apply it to the entities that matter for compliance or support forensics. Resist the urge to make it global — an audit table that logs every write is a write-amplification and PII liability, not a feature.

2. Field allow-listing. Even with an entity marked loggable, most implementations of this pattern log the whole changed field set. Before shipping, prune fields you never want duplicated into a second table: passwords, tokens, PII columns, raw blobs, timestamps like updated_at that change on every write. If the bundle doesn't expose an "ignore" mechanism, fork the listener and add one — this is one of the two design decisions you must make up front (the other is retention).

3. Reader layer, not raw queries. Wrap the log table in a repository/service that exposes the shapes you actually need:

$log->forEntity(Invoice::class, $invoice->id)
    ->latest()
    ->limit(50)
    ->get()
    ->map(fn ($entry) => [
        'at'     => $entry->createdAt,
        'actor'  => $entry->user,
        'action' => $entry->action,
        'diff'   => $entry->data, // ['status' => ['old' => 'draft', 'new' => 'sent']]
    ]);

UI code should never know the schema; when you add retention/partitioning later, only this layer changes.

4. Actor attribution — resolve it inside the write path, once. The bundle pulls the acting user from Symfony's security token. In Symfony that's free. In a Laravel port, resolve auth()->id() in the observer (not at query time), because by the time you display the log the actor is a different person. Also capture the actor for CLI/queue contexts explicitly (system user, job initiator) — otherwise the field is silently null and your "who did this?" answer is "nobody."

5. Diffing semantics — pick one and document it. The bundle records old/new per field. Two subtleties to nail down:

  • Nested/related changes. Does changing a child row through a parent's collection show up? Usually not unless the child is itself loggable.
  • Value coercion. Dates, enums, and JSON columns need a stable serialization so "old" and "new" compare correctly on read. Serialize to an explicit format (ISO8601, enum ->value) before persisting.

6. Read-path integration points. Three consumers, one data source:

  • Admin detail page: inline "history" tab on the entity view.
  • Support forensics: search by entity_id + date range, filter by actor.
  • Activity feed: query by actor or entity type, paginated, for a "Recent changes" panel.

Building the reader layer first means all three ship from the same query interface.

7. Transactions and failure isolation. The listener runs inside the same Doctrine flush as the business write. Wrap the audit write in its own try/catch (or dispatch to a queue) so a logging failure cannot roll back the business transaction. Audit logging that breaks checkouts is worse than no audit logging.

8. Testing. Write one functional test per operation (create/update/delete) asserting the row shape: action, actor, and diff content. This is your regression net for framework upgrades — and with an unmaintained bundle, you'll need it.

Gotchas and Tips

It's Symfony. This is the dominant gotcha and it invalidates any "just install it" motion in Laravel. On Laravel, don't attempt a Doctrine bridge purely to host auditing — that's a large architectural change for one concern. Reimplement the pattern.

Zero adoption = zero hardening. No dependents means no reproducible integrations to learn from and no evidence of compatibility with recent PHP/Symfony/Doctrine versions. Assume it needs patching to run at all, and test before you build on it.

Actor is often null. Security-context resolution doesn't fire in CLI commands, queue workers, and tests. Explicitly capture and set a system/automation actor; document the convention so "null" isn't ambiguous.

Value/old-vs-new drift on updates. If you store only the changed fields, reconstructing "what the record looked like at time T" requires replaying the log. That's fine for "who changed field X," but not for point-in-time restore. If you need snapshots, store the full entity state on each change and accept the storage cost.

Delete logging is the sharp edge. On deleting, the entity's attributes are still readable; after the flush they're gone. Capture the full attribute set in the delete listener, otherwise your "deleted" entries are useless ("something was deleted, we don't know what").

PII duplicated silently. Any field-level diff copies values into a second table. If User is loggable and you don't redact email/phone/address, you've created a shadow copy of personal data with different retention rules — a GDPR and storage liability. Allow-list, don't deny-list.

Bounded table growth. Audit tables grow monotonically and are never deleted by the framework. Plan retention (archive after N months), partitioning (by date or entity type), and a vacuum/prune job before the table is large. Retrofitting partitioning on a hot audit table is painful.

Listener signature drift = silent no-op. When Symfony/Doctrine bump event signatures, an unmaintained listener can stop being called without a loud error — you keep shipping, the audit trail goes dark. Add an assertion in your test suite (a write must produce a log row) and run it in CI, not just locally.

Schema coupling to domain entities. The log stores class names and possibly property names. Refactoring an entity (rename, move namespace, split) orphans its history. Either keep a stable "auditable key" on entities or accept that old log rows reference classes that no longer exist — and handle that in the reader layer.

Extension point to watch. The Entity/LogEntry mapping is where you'll want to add columns (tenant ID, request ID, IP). Fork it early if your reader layer needs those — otherwise you'll be migrating a populated audit table mid-flight.

Debugging tip. When logs aren't appearing: verify the bundle is registered, the entity is actually annotated (attribute reflection is a common silent miss), the flush occurred (listeners fire on flush, not on persist()), and the log table exists. In that order — the first two account for most "it doesn't work" cases.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource