kronhyx/tracker-bundle
kronhyx/tracker-bundle is a PHP package for adding activity tracking to Laravel applications. It provides a configurable tracker bundle for recording events and application activity, helping developers integrate simple tracking and logging into their projects.
First, verify the framework. Despite being pitched alongside Laravel packages, kronhyx/tracker-bundle is a Symfony bundle (Kronhyx/TrackerBundle). It is registered in config/bundles.php and wired through Symfony's DI container and event dispatcher, and its "track entity changes" capability almost certainly hangs off Doctrine ORM lifecycle events (prePersist, preUpdate, onFlush, postFlush). In a Laravel codebase there is no drop-in install; read Getting Started as "how to evaluate and what your minimum path looks like," and see Implementation Patterns for the Laravel translation.
If you are on Symfony + Doctrine (the only native fit):
composer.json require block: PHP floor, symfony/* floor, doctrine/orm floor.doctrine.event_subscriber / doctrine.event_listener in services.yaml).postFlush / a message.composer require kronhyx/tracker-bundle, register Kronhyx\TrackerBundle\TrackerBundle::class => ['all' => true] in config/bundles.php.Resources/config/services.yaml (or config/) — this is where the entity list, storage driver, and event wiring are configured. That file is the real API surface.Note or SupportTicket entity with few fields and no relations. Do not start with User, Order, or anything payment-adjacent — audit tables built on entities containing PII create a second data store with its own retention and erasure obligations.If you are on Laravel: treat this repository as prior art, not a dependency. Skim the Doctrine onFlush diff logic to understand the shape of the problem, then implement with spatie/laravel-activitylog (activity-feed oriented) or owen-it/laravel-auditing (audit-log oriented), or a ~150-line set of Eloquent observers if your needs are narrow. A first use case in Laravel: php artisan make:observer OrderObserver --model=Order and log getOriginal() vs getChanges() into an audits table.
Pattern 1 — Symfony: global doctrine subscriber with an opt-in entity allowlist. Do not enable tracking for every entity. Configure an explicit list (or an interface, e.g. TrackableInterface, implemented by entities that should be audited). Blanket tracking on a mature schema means an audit table that clones your whole database at 5–10× write volume.
# config/packages/tracker.yaml (verify key names against the bundle source)
tracker:
enabled: true
entities:
- App\Entity\SupportTicket
- App\Entity\Account
ignore_fields: ['updatedAt', 'updated_at', 'version']
Pattern 2 — Symfony: transaction-aware write ordering. The classic Doctrine pitfall is writing audit rows during onFlush while the entity diff is still mutable. Compute the diff in onFlush (that's where $uow->getEntityChangeSet() is valid), then persist the audit rows in onFlush via $uow->computeChangeSet() on the audit entity — or defer to postFlush / a message for lower coupling. Whether this bundle does either is the single most important thing your source read should establish.
Pattern 3 — Symfony: actor context from a request-scoped provider, with an explicit fallback. Audit rows need user_id. In HTTP this comes from TokenStorageInterface. In a messenger:consume worker or a console command it is null, and a null actor is materially different from system. Push actor context onto the queue message (e.g. a StampsAuditActor message stamp) rather than trying to read the token in the consumer.
Pattern 4 — Laravel translation: Eloquent observer + queued write.
// app/Observers/AuditObserver.php
public function updated($model): void
{
$changes = collect($model->getChanges())
->except(['updated_at'])
->map(fn ($new, $field) => [
'field' => $field,
'old' => $model->getOriginal($field),
'new' => $new,
])->values();
if ($changes->isEmpty()) {
return;
}
AuditLog::create([ // or dispatch(new WriteAuditLog(...)) if async is acceptable
'auditable_type' => $model::class,
'auditable_id' => $model->getKey(),
'actor_id' => auth()->id(), // null in jobs/commands — be explicit
'changes' => $changes,
'created_at' => now(),
]);
}
Register per-model (Order::observe(AuditObserver::class)) rather than globally; global observation in Laravel is a footgun for seeding, imports, and migrations.
Pattern 5 — Read side: build the timeline query you actually want. The package captures writes; the value is in the detail-page timeline. Design the read query first (where auditable_type/auditable_id order by id desc limit 50, indexed on (auditable_type, auditable_id, id)). If the bundle ships no query API, you will write it yourself — budget for that.
Workflow tip: put tracking behind a feature flag per entity. Roll out one entity at a time over a couple of weeks and compare row counts against write counts (SELECT COUNT(*) FROM audits WHERE ... vs. your app's mutation logs). Gaps you can't explain are the failure mode that matters for a compliance-adjacent subsystem.
UnitOfWork diffing → Eloquent getChanges()/getOriginal(), EventSubscriber → observers, services.yaml → service provider bindings. Estimate 2–3 engineer-days for a focused spike; if that estimate is uncomfortable, you are better served by a maintained alternative.null. Plan to fork under your own namespace and own it; the MIT license permits it, but the cost lands on your team. Assume it has been tested against exactly one Symfony/Doctrine combination.flush() inside a listener will bite you. Calling $em->flush() from within onFlush recurses. Use $uow->computeChangeSet(), or defer to postFlush/KernelEvents::TERMINATE/a message. If the bundle flushes in onFlush, that's a red flag.getEntityChangeSet() only sees owning-side scalar changes. OneToMany additions/removals and inverse-side updates typically do not appear. Verify against your domain: an Order gaining OrderLines is often the most interesting audit event and the most likely to be missed.updated_at, version/@Version columns, and any last_seen_at-style telemetry columns, or you will get an audit row for every touch and your timeline becomes useless. Note that Doctrine's @Version bump will otherwise appear as a "change."['status' => 'draft'] as JSON means historical rows break when you rename or remove a field. Prefer storing raw old/new values keyed by field name and rendering in the UI, not pre-rendered strings. Version your audit payload shape (schema_version: 1) — it costs nothing now.ignore_fields on email, phone, national_id, password_hash) rather than relying on a future erasure job. "Right to erasure" must now also delete from the audit table — decide whether that destroys your audit-integrity story before you build it.pg_partman. If the bundle has no retention hook, add a scheduled prune job.TokenStorage is empty in workers, console commands, and onKernelTerminate after the session closes. Every audit row written from a queue will show null unless you explicitly carry the actor. This is the most common "why does the audit log look wrong" bug.INSERT into the audit table. That tells you in ten seconds what the actor, diff, and transaction boundary actually are — faster than reading the bundle source.How can I help you explore Laravel packages today?