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

Audit Kit Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. Despite the "audit kit" name, this is a Symfony bundle (DI extension + Doctrine event subscribers), not a framework-agnostic library. In Laravel it will not auto-register, has no service provider, and its services are wired through Resources/config/services.yaml and the Symfony container. If your project is Eloquent-based, stop here: nothing in this package touches Eloquent models. If you run Doctrine in Laravel (laravel-doctrine/orm), bridging is possible but manual — see Implementation Patterns.

Where to look first, in this order:

  1. README.md — attribute/interface names and the config tree are the whole public API; this bundle is thin enough that the README is nearly the full documentation.
  2. src/DependencyInjection/Configuration.php — the authoritative list of config keys and defaults. Trust this over the README if they disagree.
  3. src/Resources/config/services.yaml — the exact service IDs / listener classes you must reference if you wire it by hand (Laravel or a non-autoconfigured Symfony app).
  4. src/EventListener/ — this is where behaviour actually lives; reading it takes minutes and answers every "when exactly does updatedAt change?" question.
  5. CHANGELOG.md / UPGRADING.md — with 1 star, 0 dependents and a 2026 release, this is your only signal on stability and BC policy.

Minimal setup (Symfony):

composer require nowo-tech/audit-kit-bundle
// config/bundles.php
Nowo\AuditKitBundle\NowoAuditKitBundle::class => ['all' => true],
# config/packages/nowo_audit_kit.yaml  — verify key names against Configuration.php
nowo_audit_kit:
    # field names, user resolver, table prefix, etc.

First use case — one opt-in entity, end to end:

#[ORM\Entity]
#[Auditable]              // opt-in marker: attribute, interface, or trait — confirm in src/
class Invoice
{
    // createdAt / updatedAt / createdBy / updatedBy added by the bundle
}

Then generate and inspect the migration rather than assuming it:

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

Create one row, update it, and dump the columns. That single round-trip validates 90% of the package's value. Everything else is configuration.

Verdict up front: the scope is genuinely useful and narrow — timestamps plus blame, opt-in per entity, no schema-wide magic. That is the right design for an audit concern. The risk is maturity (single maintainer, no dependents, no third-party usage to learn from), so budget an hour to read src/ before committing to it in production.

Implementation Patterns

Opt-in at the entity level, never globally. The package's core promise is that only annotated entities are affected. Keep it that way: apply the attribute to the entities that need an audit trail (Invoice, Order, Subscription), not to lookup tables or high-churn log tables. Every annotated entity gets extra columns and an extra listener pass on flush.

Two separate concerns, enable them independently. Timestamps and blame have different failure modes. Timestamps are safe everywhere; blame is only meaningful when there's an authenticated actor. Turn on timestamps first, ship it, then add blame once you've decided what a null user means (see gotchas).

Custom blame resolver is the extension point you'll actually use. A well-built version of this bundle resolves the actor from the Symfony token/storage. In a Laravel/Doctrine setup you'd have to supply the equivalent:

// laravel-doctrine config/doctrine.php — register whatever listener ID the bundle exposes
'managers' => [
    'default' => [
        'events' => [
            'subscribers' => [
                \Nowo\AuditKitBundle\EventListener\AuditListener::class, // confirm exact class in services.yaml
            ],
        ],
    ],
],

If the bundle hard-codes a Symfony service for the user, the cleanest escape hatch is a small adapter subscriber of your own that stamps the fields from Auth::id(). Write that adapter behind your own interface (AuditActorResolver) so the third-party dependency stays at one call site.

Do not rely on it for a real audit trail. This records who last touched the row, not what changed. If you need history (old value → new value, per field), you want an activity-log / versioning layer alongside it. Teams routinely conflate the two and discover the gap during their first incident review. Treat this as bookkeeping metadata, not compliance evidence.

Backfill deliberately. Existing rows get NULL timestamps. Don't run a blanket UPDATE ... SET created_at = NOW() — you'll be silently destroying the one signal that distinguishes pre-migration data. Backfill from a real source column where one exists, and leave the rest null.

Testing pattern. Assert on the persisted row, not on the entity object — the fields are set during flush, so a getter before flush() proves nothing:

$em->persist($invoice);
$em->flush();
$em->refresh($invoice);

$this->assertNotNull($invoice->getCreatedAt());
$this->assertSame($user->getId(), $invoice->getCreatedBy()->getId());

For deterministic assertions, freeze the clock (symfony/clock's ClockMock, or Carbon's setTestNow in Laravel) and mock the actor resolver — otherwise these tests flake at midnight and pass locally because someone's logged in.

Gotchas and Tips

Symfony-only container wiring. No Laravel service provider, no autodiscovery. In a Laravel app you're manually constructing the listener and injecting an EntityManager. That's two lines of glue if the bundle's classes have clean constructors — check src/EventListener before you commit, because a listener taking TokenStorageInterface and AuthorizationCheckerInterface is painful to satisfy outside Symfony.

Eloquent is entirely out of scope. If any part of the assessment assumed "Laravel model", it doesn't apply. Laravel already gives you created_at/updated_at for free and adds blame with ~10 lines of creating/updating model events. For an Eloquent codebase, this package is strictly more work than the native solution.

Bulk DQL/DBAL operations bypass lifecycle events. UPDATE Invoice i SET ... will not touch updatedAt or updatedBy. Same for ->execute() on a query builder update. Any bulk path needs the fields set explicitly, and this is the single most common source of "why is updated_at stale?" bug reports.

No authenticated user in console/queue/tests. Cron jobs, workers, seeders, and imports all lack a token. Decide the policy before you ship: nullable columns (recommended) or a designated "system" user row. Don't let it throw — a fatal in a queue worker is a bad way to discover this.

updated_at may not change on a no-op update. Doctrine only fires preUpdate when the changeset is non-empty. If setter-driven code assigns identical values, the timestamp stays put. Some implementations force the update (extra query, and optimistic-lock version bumps); some don't. Verify the behaviour in src/ and document it for your team.

Timezone mismatch. The bundle will use PHP's DateTimeImmutable; if your DB column is TIMESTAMP and the session timezone differs, comparisons against NOW() are off by hours. Use DATETIME/TIMESTAMPTZ at UTC and keep the application timezone UTC.

Version pinning is non-negotiable here. One star and zero dependents means no downstream canary. Pin an exact version ("nowo-tech/audit-kit-bundle": "1.0.0"), commit the lock file, and read the diff before every bump. Wrap your usage — entity attributes plus one adapter service — so a package replacement is an hour of work, not a refactor.

Debuggability. If fields aren't populating, check in this order: (1) is the entity actually opted in — typo in the attribute/interface is silent; (2) did the listener get registered — inspect bin/console debug:event-dispatcher doctrine.eventListener (or doctrine.event_subscriber) and confirm it's listed; (3) listener priority — another subscriber mutating entities earlier can invalidate the changeset; (4) is updatedAt already non-null and the code only setting it on insert? A quick $em->getUnitOfWork()->getScheduledEntityUpdates() in a test tells you whether Doctrine even intends to write.

Migrations are yours, not the bundle's. There is no schema-generation step. If you add the attribute to an existing entity and forget migrations:diff, you get a runtime "column not found" only on the paths that touch the entity — often in production first. Add the migration in the same PR as the attribute, always.

Config key drift. With a single-maintainer bundle released recently, treat README examples as documentation of intent rather than contract. src/DependencyInjection/Configuration.php is the source of truth; when they diverge, the config tree wins and the README is a bug you can report upstream.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource