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

Doctrine Log Bundle Laravel Package

manasbala/doctrine-log-bundle

A Symfony bundle for audit logging Doctrine entity changes. It captures create, update, and delete events and stores a change history for traceability, integrating through Doctrine event listeners with configurable logging options.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle wrapping Doctrine ORM, not an Eloquent package — if you're coming from Laravel, the mental model is "model observer / auditable trait," but it only works inside a Symfony kernel with Doctrine's event system. Plan for that before you install it.

Release 1.0.7 added support for JMS Serializer 4, which confirms what the earlier source already implied: the changed-field payload is serialized through jms/serializer, not json_encode. That matters from day one — the shape of your audit rows is governed by JMS metadata (exclusion policies, serialization groups, naming strategies), not by the entity itself. Check your JMS version before installing.

Minimal path:

composer require manasbala/doctrine-log-bundle
composer require jms/serializer:^4.0   # 1.0.7 adds JMS 4 support; 3.x is likely still allowed

Register it (Symfony Flex may do this for you in config/bundles.php):

return [
    // ...
    ManasBala\DoctrineLogBundle\DoctrineLogBundle::class => ['all' => true],
];

Look here first, in this order — the package has ~8 stars and docs are thin, so the source is the documentation:

  1. vendor/manasbala/doctrine-log-bundle/src/DependencyInjection/Configuration.php — the authoritative list of config keys. Don't trust blog posts. Pay attention to any serializer-related keys added around 1.0.7.
  2. src/EventListener/ — shows exactly which Doctrine hooks (onFlush, postPersist, postUpdate, postRemove) it subscribes to. This tells you what it can't see (see Gotchas).
  3. Wherever the bundle calls SerializerInterface::serialize() — this is the seam you own. Grep for jms / Serializer in src/ to find it.
  4. src/Entity/ (or src/Model/) — the log record shape: entity class, id, action, changed payload, timestamp, actor.

Then generate the log table:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

First use case: an audit trail on one high-value entity — User, Invoice, or Order. Pick a single entity first, verify the rows land correctly, then broaden. Turning it on globally across a legacy schema is how you get a 40GB log table in a week.

Implementation Patterns

Opt-in per entity, not globally. Most bundles of this shape let you either annotate classes or list them in config. Start with an allowlist:

# config/packages/doctrine_log.yaml — confirm exact keys in Configuration.php
doctrine_log:
    enabled: true
    entities:
        App\Entity\Invoice: ~
        App\Entity\User: ~

Keep log writes in the same transaction as the change. The listener runs during onFlush, so the log entry commits or rolls back with the business write. Don't wrap business logic in an outer transaction that a separate log connection can't see — you'll get audit rows for changes that never happened.

Control the payload through JMS metadata, not through hacks. Since 1.0.7 the bundle serializes via JMS, so the cleanest way to shape what lands in the log is JMS itself:

use JMS\Serializer\Annotation as Serializer;

class Invoice
{
    #[Serializer\Exclude]           // never leaves the entity manager on the way to the log
    private string $apiToken;

    #[Serializer\Groups(['audit'])] // only logged because the audit group asks for it
    private string $status;
}

Configure the serializer context (groups, ExcludePolicy::EXCLUDE_NONE or the default exclude-all policy) once, deliberately. Default JMS behavior is "exclude everything unless told otherwise," which is safer than the opposite — but it also means a missing annotation silently produces empty payloads, which looks like a broken bundle rather than a metadata problem.

Attribute actor context at request boundaries. The bundle can't know who made the change. Set a request-scoped "current user" (a lightweight subscriber on kernel.request, or the bundle's user-resolver hook if it exposes one) so every row carries a username/user_id. Without this, your audit log answers "what changed" but never "who."

Build the read side deliberately. Treat the log table as an append-only event stream and query it, don't hydrate entities:

$logs = $logRepository->createQueryBuilder('l')
    ->where('l.objectClass = :class')
    ->andWhere('l.objectId = :id')
    ->setParameter('class', Invoice::class)
    ->setParameter('id', $invoice->getId())
    ->orderBy('l.loggedAt', 'DESC')
    ->getQuery()
    ->getResult();

Add a composite index on (object_class, object_id, logged_at) in your migration — the bundle won't do it for you, and this is the only query pattern that matters.

Redact before you persist. Two layers, use both: JMS #[Serializer\Exclude] on the sensitive properties (cheap, metadata-driven, survives refactors) and a decorator around the bundle's listener for anything dynamic like "mask the last four digits of a card number." Never rely on post-hoc scrubbing of the log table — by then it's already written and probably already backed up.

Pair it with a diff view in your admin UI. The old/new payload is only useful if a human can read it. A simple "field / before / after" table per log row converts this from a database curiosity into something support actually uses.

Gotchas and Tips

  • 1.0.7 is a compatibility release, not a feature release. It widens the JMS Serializer constraint to 4.x. Nothing in the bundle's own surface changed, but it does mean the repo isn't abandoned — which softens, but doesn't erase, the "8 stars, thin docs, verify everything" caveat. Read composer.json on the tag to see the exact constraint range; if it allows both 3.x and 4.x, your composer.lock decides your behavior, and CI and prod should agree.
  • JMS 4 metadata migration. Moving from JMS 3 to 4 is where this release bites: annotation-based metadata handling shifted toward attributes, and if your entities or the bundle's own DTOs still carry docblock annotations, serialization can silently produce empty or partial payloads. Run one end-to-end update through the audit path before merging the dependency bump, and diff the resulting JSON against a row written on the old version.
  • Payload schema drift. Existing log rows are JSON written by whatever serializer version was installed at the time. A JMS major bump can change naming strategies, date formats, or null handling — so a single entity's history may contain two different JSON shapes. Either normalize on read, or pin the serializer version for the lifetime of the audit data (which is usually "forever").
  • Annotation-era code elsewhere. A bundle of this vintage may map its own log entity with docblock annotations. On PHP 8 with Doctrine ORM 3, Doctrine annotations are gone — check the mapping driver and be ready to convert to attributes. This is separate from the JMS question and both can bite at once.
  • onFlush blind spots. Collection changes (many-to-many add/remove, owning-side updates) are the classic miss for listeners at this hook. Test a $order->addLineItem() flow, not just scalar field updates.
  • Recursive flush danger. If the listener persists its own log entity inside onFlush, verify it uses computeChangeSet/scheduleForInsert correctly rather than calling flush() — a nested flush will either throw or lose data.
  • No retention policy ships with it. Plan pruning from day one: a scheduled DELETE FROM log WHERE logged_at < :cutoff, or Postgres partitioning if volume is high. This is the #1 operational failure mode for audit bundles.
  • Rollback semantics. If you ever split the log write onto a second entity manager or connection, rolled-back business transactions will still leave audit rows. Keep one EM.
  • Debugging when nothing is logged: three suspects, in order — the listener isn't registered (bin/console debug:event-dispatcher doctrine.event_listener / doctrine.event_subscriber), the entity isn't in your allowlist, or JMS excluded every property so the payload is []. The last one is new as of this release line and is the easiest to misdiagnose as "the bundle is broken." The package's config tree is the ground truth.
  • Extension points to prefer: replace the log entity class via config rather than editing vendor code; decorate the listener for dynamic redaction; keep your own repository on top of the log entity for query logic. That keeps you upgrade-safe if you later swap to gedmo/loggable or a maintained fork.
  • Alternatives worth benchmarking first: doctrine-extensions Loggable (Gedmo) is far more battle-tested and doesn't drag in a serializer. Writing your own onFlush subscriber for a single entity is ~60 lines. This bundle's value is convenience, not depth — if you need field-level history on many entities, read the source before committing to 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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core