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 Changelog Bundle Laravel Package

softspring/doctrine-changelog-bundle

A Symfony bundle for Softspring Doctrine Changelog that automatically records entity changes and stores an audit trail of inserts, updates, and deletes. It integrates with Doctrine ORM to help track data history, surface modifications, and support compliance and debugging workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: softspring/doctrine-changelog-bundle is a Symfony bundle for Doctrine ORM, not a Laravel package. There is no service provider, no config publish tag, no Artisan command, no Eloquent integration. It attaches a Doctrine event listener that computes field-level before/after diffs on flush and persists them to a changelog table, keyed by entity FQCN + id, with a timestamp and (configurable) user identifier.

That splits your path in two, and you should decide before writing code:

  1. Your app already runs Doctrine ORM entities inside Laravel (e.g. you use doctrine/orm alongside or instead of Eloquent) → you can consume the bundle by wiring its services manually in a small service provider.
  2. Your app is Eloquent-first (the normal Laravel case) → do not adopt this as a dependency. Changelog rows will exist only for the handful of Doctrine entities you have, and you'll own a two-ORM app to get an audit trail. Take the pattern and use owen-it/laravel-auditing or spatie/laravel-activitylog. This package is still worth reading as design prior art — its diff-payload schema is a good shape to copy.

Assuming path 1, the minimal sequence:

  1. composer require softspring/doctrine-changelog-bundle — and open the AGPL-3.0 license before you go further (see Gotchas).
  2. Confirm your Doctrine EntityManager, metadata driver, and migrations pipeline actually work. The bundle assumes a functioning Doctrine setup; a half-installed Doctrine is the most common reason a first attempt does nothing.
  3. Read vendor/softspring/doctrine-changelog-bundle/src and its DependencyInjection/Configuration.php. The README is thin and the release notes are empty; the config tree is the real documentation.
  4. Create the changelog table — via doctrine:migrations:diff from the bundle's own changelog entity (Symfony path), or by mirroring its columns in a Laravel migration if you're bridging.
  5. Bind the listener's user resolver to something real (Auth::id() for HTTP, an explicit actor for jobs/console), otherwise you get a table of anonymous rows.
  6. Track exactly one entity, flush a change, and inspect the row. That's the first use case: "who changed this invoice, and from what value" for a record where a support rep or auditor will actually ask the question.

Implementation Patterns

Track a short, deliberate list. The bundle is driven by a configured entity list. Five entities maximum at launch. Audit tables are append-only and grow with every write — the cost is real and permanent.

Symfony config is the native surface:

# config/packages/doctrine_changelog.yaml
doctrine_changelog:
    entities:
        App\Entity\Invoice: ~
        App\Entity\Contract: ~

In a Laravel+Doctrine bridge, port the meaningful keys into config/changelog.php and construct the listener yourself:

// AppServiceProvider::register()
$this->app->singleton(ChangeLogSubscriber::class, function ($app) {
    return new ChangeLogSubscriber(
        $app->make(EntityManagerInterface::class),
        config('changelog.entities'),
        fn () => Auth::id() ?? 'system',   // never let this be null
    );
});

// after the EntityManager is built
$em->getEventManager()->addEventSubscriber(
    $this->app->make(ChangeLogSubscriber::class)
);

Keep this in a single provider — it's the seam that breaks on upgrade.

User resolution must be a closure. The same code path has to serve HTTP (Auth::user()), queued jobs (the job's actor, not the currently-authenticated user, which is null in a worker), and console/backfills (an explicit system marker). Hardcoding Auth::id() is how you end up with a majority-anonymous audit log.

One diff presenter, reused everywhere. The payload is field → [old, new]. Write a single presenter that renders "status: draft → sent", handles inserts (old is null) and deletes as explicit cases, and is used by the detail page, exports, and notifications. Don't let three Blade templates each invent their own null handling.

Index for the only query you'll run. "History of this record, newest first":

$t->index(['entity_class', 'entity_id', 'created_at'], 'changelog_entity_history');

Missing this composite index is the first performance complaint, usually around month three.

Enrich asynchronously. During flush you don't have the new entity's id for inserts, and you shouldn't run extra queries inside the unit of work. Write a minimal row synchronously, then enrich (request id, IP, source system, resolved display labels) in a queued job keyed on the changelog row's own id.

Retention from day one. Ship a scheduled changelog:prune --days=365 command in the first PR. Nobody ever deletes from an audit table until storage is an incident.

If you need revert/restore, verify it in the source before promising it. Restoring an entity to a prior version is the feature stakeholders get excited about, but it means replaying old values back through Doctrine, and relation/collection semantics are where those flows break. Read the code and write a test before it appears on a roadmap slide.

Gotchas and Tips

  • AGPL-3.0 is a hard gate, not a footnote. This is a strong copyleft license with a network-use clause. Bundling it into a SaaS or commercial product usually triggers source-disclosure obligations for the combined work. Get a legal read before the spike, not after — and note that "we only use the pattern, not the code" is a meaningfully different (and safer) position.
  • The Symfony plumbing is dead code in Laravel. *Bundle, the DI extension, and its YAML tree do nothing under Laravel. Don't burn an afternoon trying to make config/packages/*.yaml load. Documented configuration is only partially applicable to you.
  • Flush timing is the #1 source of confusing rows. Diffs are computed on onFlush, before SQL runs; for new entities the identifier may not exist yet and the insert lands on postFlush. Null entity ids on insert rows are this, not a bug in your wiring.
  • Mass updates are invisible. DB::table()->update(), raw SQL, Model::query()->update(), and any write outside a managed Doctrine entity produce no changelog row. If compliance assumes full coverage, this is the hole to document loudly and loudly in the runbook.
  • Redact before you persist. Old/new values are stored verbatim. Password resets, API-key rotations, and remember-tokens will happily write the previous hash or secret into the table in plain text. Maintain an explicit ignore/redact list per entity and re-review it whenever an entity changes.
  • Column types matter. The diff payload needs TEXT/LONGTEXT/JSON, never VARCHAR(255). Rich-text fields and serialized blobs truncate silently.
  • Ignore the noise fields aggressively. updated_at, created_at, sort columns, version counters, cache keys. Otherwise a single related-item edit looks like a change to the parent and the timeline becomes unreadable.
  • Class renames split history. Rows are keyed by FQCN string. Moving App\Models\Invoice to App\Domain\Billing\Invoice orphans the old trail — pair the rename with a data migration.
  • Don't let audit failure roll back a business write. Wrap the recording path in try/catch and log, unless the audit row is legally required to block.
  • Restart queue workers after config deploys. Cached config plus singleton listeners means "changelog randomly stopped working after the deploy" is almost always stale workers.
  • Low adoption, so be your own QA. One star. Treat every upgrade as unsupported. Pin the version, read the actual diff, and check composer.json constraints against your PHP, doctrine/orm, and symfony/* versions — Doctrine ORM 3.x compatibility has been an evolving area.
  • v5.6.0 gives you nothing to read. The notes are only a compare link. It's a routine-looking minor, but that's an absence of information, not a clearance. Verify the changelog table schema gained no required columns before bumping.
  • Extension point worth knowing: storage is pluggable via a custom changelog store. If you outgrow the table (write amplification on a hot entity, or you need to ship entries to a warehouse), that's the seam to swap rather than forking the listener.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource