nowo-tech/audit-kit-bundle
A Symfony bundle for adding audit trails to Doctrine entities. It automatically records create, update, and delete events, capturing changed fields, user context, and timestamps so teams can review who changed what and when.
Recommendation: Conditional No-Go for adoption as a dependency — treat as a reference implementation only.
The package solves a genuinely recurring problem (audit columns: createdAt/updatedAt plus createdBy/updatedBy) with a defensible design choice: opt-in per entity rather than global interception. That limits blast radius and makes the change reviewable. Everything else about the package argues against taking it as a runtime dependency.
Architecture fit. The bundle is Symfony + Doctrine ORM, wired as a Symfony bundle with DI extension and Doctrine event subscribers. It is not a Laravel package and has no Eloquent equivalent — the request framing as a "Laravel/PHP package" is a mismatch that must be resolved before any evaluation proceeds. If the target codebase is Laravel, this is a hard stop; the pattern would be reimplemented as a model trait plus observers (or a BelongsToUser-style concern plus a creating/updating hook), which is roughly 100–200 lines. If the target is Symfony/Doctrine, the bundle is directionally appropriate but unproven.
Integration feasibility. Mechanically low effort: register the bundle, enable the mapping/config for selected entities, add four nullable columns plus a migration, and ensure a security token storage is available. The friction is not in the wiring, it is in the semantics — blame fields have no defined value in CLI commands, message consumers, fixtures, and unauthenticated requests, so nullability and downstream reporting must be designed up front.
Technical risk. Dominated by supply-chain and provenance signals rather than code complexity:
EventListener/EntityListener is a bounded, well-understood piece of code that the team already owns.Key questions for the maintainer / spike:
TokenStorageInterface with null-safe fallback, or something that throws in CLI context?preUpdate/prePersist only, or also onFlush recomputation for changed-field detection (updatedAt should not churn on no-op updates)?new \DateTimeImmutable() inline (untestable, timezone-dependent)?#[Embeddable] cases?Stack fit. Only viable where the application is Symfony 7/8 on PHP ≥8.2 with Doctrine ORM and an authenticatable user provider. Where the estate is Laravel/Eloquent, or mixed with raw DBAL/query-builder writes, this package does not fit and should not be forced.
Migration path. Non-destructive if sequenced correctly: the audit columns are additive and nullable, so rollout can be dark. Avoid making them NOT NULL until every write path (including seeders, imports, and async workers) is covered. For legacy rows, backfill with a documented sentinel (system, import) rather than null so that reporting queries stay simple.
Compatibility. Two axes to validate in a spike before commitment: (a) Symfony minor-version drift — bundles frequently break on major upgrades via changed event ordering or DI autoconfiguration; (b) Doctrine ORM 3.x vs 2.x event API differences, which are a common source of silent no-ops. Also verify coexistence with other Doctrine subscribers already in the app (soft-delete, timestampable from other vendors, slug generators) — listener ordering is not guaranteed and duplicate Timestampable behaviour will produce conflicting writes.
Sequencing.
Maintenance. The real cost is not the dependency's code, it is owning an unmaintained transitive surface. With one star and zero dependents there is no realistic expectation of upstream fixes; plan to vendor or fork on day one, which converts "adopt a package" into "own a small internal library with an upstream reference." At that point the honest comparison is: fork-and-own versus write-120-lines, and the latter usually wins on clarity.
Support. No commercial support, no published SLA, no security disclosure process identified. For audit columns specifically — which touch compliance-adjacent data ("who changed this record?") — an unmaintained dependency in the write path is a poor trade. Where the data is used for regulatory or incident forensics, treat this as a compliance-relevant component requiring a named internal owner regardless of who wrote the code.
Scaling. Runtime cost is negligible: one listener invocation per managed entity per flush, negligible against the query cost of the flush itself. The scaling risk is organisational and schema-level — the same columns replicated across many entity managers, multi-tenant schemas, or read replicas, where a single missed migration produces intermittent write failures in production rather than at deploy time.
Failure modes to instrument for:
UPDATE, DBAL, and raw SQL bypass lifecycle callbacks; records mutate with stale audit values. This is the highest-severity failure because it is invisible. Mitigation: route privileged writes through the ORM, and add a periodic reconciliation check.createdBy = null; downstream reporting that assumes non-null breaks quietly. Decide the sentinel value before rollout.updatedAt.prePersist/preUpdate fail the whole flush and surface as opaque errors far from the audit layer.preUpdate handlers bump updatedAt on every flush, polluting change-detection and breaking cache-invalidation heuristics keyed on that column.Ramp-up. Low in absolute terms — half a day for a Symfony engineer familiar with Doctrine events, one to two days for someone who is not, mostly spent on the null-actor and bulk-write edge cases. The documentation surface is small enough to read in full, which is itself a signal: a package this small does not amortise a dependency-management overhead across enough value.
Bottom line: approve only if (a) the estate is confirmed Symfony/Doctrine, (b) the spike passes on the two target entities, (c) an internal owner is named, and (d) the future-dated release is explained. Otherwise adopt the pattern — opt-in audit columns via a first-party subscriber with an injected clock and null-safe actor resolution — and skip the dependency.
How can I help you explore Laravel packages today?