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.
Architecture fit. The package is a Symfony bundle that hooks Doctrine ORM's unit of work, computes field-level before/after diffs on selected entities, and persists them with an author and timestamp. The architectural fit for a Laravel/Eloquent estate is therefore poor as a dependency and good as a pattern. It has no awareness of Eloquent models, model events, DB::table() writes, or raw SQL, so on a typical Laravel service it would observe nothing at all. What transfers is the design: an opt-in per-entity marker, a synchronous diff writer on flush, a callable user resolver, and a separate presenter layer for rendering history. Treat the repository as reference architecture, not a component to install, unless the target service is a Symfony app or a Laravel app already running Doctrine as its persistence layer.
Integration feasibility. Two distinct paths, with very different costs. (1) Native adoption — only viable if the service is Symfony + Doctrine, or Laravel + Doctrine bridge. Then integration is a composer require, bundle registration (or manual listener/subscriber binding in a service provider), a changelog table migration, and a user resolver pointed at Auth::id(). Days, not weeks, for a single entity. (2) Laravel-native adoption — requires manual re-implementation: bind the listener and its collaborators, map the bundle's config tree into a plain config/changelog.php, hand-write the schema to match the bundle's changelog entity, and accept that the documented configuration surface is only partially applicable. That is a spike plus a sprint, at which point a Laravel-native package is the better buy. For our purposes, the feasible path is path (1) or "pattern reuse."
Technical risk. This is where the TPM should spend the most attention, because the risks are concentrated and mostly non-technical in nature.
onFlush, identifiers materializing on postFlush) is where integrations break silently.Key questions. These should be answered before any commitment:
Stack fit. Score the package against the actual runtime, not the marketing description. If the estate is Laravel/Eloquent — which the framing of this evaluation implies — the correct decision is to not install it and instead adopt owen-it/laravel-auditing or spatie/laravel-activitylog, both of which have the community, license clarity, and Eloquent-native hooks this package lacks. Borrow three ideas from this repository regardless: opt-in per-entity tracking via an interface, a user-resolver closure rather than a hardcoded auth source, and a dedicated presenter that turns a diff payload into a human timeline. If the estate genuinely has Symfony services, this bundle is a legitimate candidate for those services only, and the Laravel decision should be made independently.
Migration path. The package is additive — one table, one listener, config entries — so adoption is reversible in principle. The migration path is: (a) create the changelog table via doctrine:migrations:diff on the bundle's own entity, or mirror those columns in a Laravel migration (entity class, entity ID, action, JSON/TEXT diff payload, user identifier, created-at); (b) register the listener; (c) enable exactly one entity; (d) instrument and compare against a hand-written expectation for a week. The rollback path is a config change plus a down-migration, but note the data does not roll back with it — history accumulated under a license or vendor we later reject still sits in our database.
Compatibility. Verify four things concretely rather than reading release notes: the composer.json constraints for PHP, doctrine/orm, and symfony/*; the schema of the changelog table across the 5.5.x → 5.6.0 boundary (did it gain required columns?); behavior under Doctrine ORM 3.x; and behavior under our auth stack's user resolver, including queue workers where the authenticated user is null. Pin the exact version in composer.json — do not float. If the resolver returns null in a worker, every async-originated change records as anonymous, which quietly destroys the value of the audit trail for exactly the writes most likely to be questioned.
Sequencing. A defensible order:
(entity_class, entity_id, created_at), redaction list defined, presenter written.Maintenance. The package is small and additive, so code maintenance is low — but the ownership burden is not, because it lands entirely on us. Budget for: a pinned dependency that will not receive timely security patches, a changelog file that has historically lived in commit messages rather than curated notes, and Doctrine compatibility work that arrives with Doctrine's release cadence rather than ours. A realistic estimate is a recurring slice of one engineer's attention per quarter, plus a documented fork-or-freeze contingency if the maintainer stops.
Support. There is no commercial support and effectively no community. Internal support therefore means one or two engineers who understand Doctrine unit-of-work timing well enough to debug missing or mistimed changelog rows. Make that a named-person dependency rather than "the team," and write down the three failure signatures (null entity ID on inserts, missing rows on mass updates, noisy association diffs) so on-call can triage without the original author. User-facing support improves materially: "why did this record change" tickets get answered from a timeline view instead of a database archaeology exercise, and MTTR on data incidents drops because point-in-time state is reconstructable.
Scaling. Two axes to plan for explicitly, because the package itself plans for neither.
TEXT/LONGTEXT/JSON — VARCHAR(255) will silently truncate rich-text changes and produce an audit trail that is confidently wrong.Failure modes. Enumerate these in the runbook, each with a detection signal:
| Failure | Detection | Mitigation |
|---|---|---|
| Listener error rolls back a business write | Elevated 5xx on tracked endpoints | try/catch + log the recording path unless the audit row is legally mandatory |
| Inserts record null entity ID | Changelog rows with null IDs | Handle inserts in the post-flush pass |
| Silent coverage gaps | Diff between row counts and business operations | Document loudly that query-builder/raw SQL writes are invisible |
| Secrets persisted verbatim | Regex scan of the changelog payload | Maintained redaction list, reviewed on every entity addition |
| Anonymous authors from queues/console | Null user IDs in worker-originated rows | Resolver closure that takes the job's actor, plus an explicit system marker |
| Orphaned history after refactor | Two histories for one conceptual entity | Rows are keyed by FQCN string — treat class renames as data migrations |
| Stale config after deploy | Changelog behavior "randomly" reverting | Restart queue workers; treat the listener binding as the fragile seam |
Ramp-up. Expect one engineer-day to first value if the stack is Symfony/Doctrine, and roughly a sprint if we are emulating the pattern in Laravel. The learning is not in the package API — it is in Doctrine's flush lifecycle and in building the presenter. Invest half a day in a written demo: one entity, one detail page, one revert-or-not decision, and a walkthrough of the diff renderer. Two things must be communicated to stakeholders at the point of adoption, because they will be asked about later and the answers should not be improvised: the audit trail covers ORM-mediated changes only, and retention/archival is our responsibility, not the package's. Framing the decision as "we are adopting an audit pattern and choosing a component per service, with AGPL and maintenance risk explicitly scored" is defensible; framing it as "we bought an audit solution" is not, given a one-star, AGPL-licensed, single-maintainer bundle with empty release notes.
How can I help you explore Laravel packages today?