madmis/activity-log-bundle
A Symfony bundle for tracking user activity and entity changes. It provides configurable activity logging, Doctrine event integration, and a simple API to record, retrieve, and display activity history in your application.
Read this first: it is not a Laravel package. madmis/activity-log-bundle is a Symfony bundle (AppKernel registration, YAML service config, Doctrine annotations). It extends gedmo/doctrine-extensions' Loggable listener to record who made a change alongside the versioned diff. There is no service provider, no facade, no config publish, and nothing Composer will autodiscover in a Laravel app.
For a Laravel developer the useful move is therefore to read it as a reference implementation, not a dependency:
LoggableListener (or whatever it subclasses from Gedmo) and its log-entry entity. The delta over stock Gedmo is small: an extra username column populated from the Symfony security token at flush time.If you were in a Symfony 2.8/3.x app, minimal setup would be: composer require madmis/activity-log-bundle, register the bundle in AppKernel, enable gedmo/doctrine-extensions' Loggable listener, import the bundle's routing/service config, annotate entities with @Gedmo\Loggable / @Gedmo\Versioned, then doctrine:schema:update to create the log table. Laravel has no equivalent of any of those four steps.
The Laravel-native starting points, in order of fit:
owen-it/laravel-auditing — closest match to what this bundle does: per-field old_values/new_values, causer (user) tracking, per-model opt-in via a trait, revert support.spatie/laravel-activitylog — event/description style with a properties JSON blob and batch UUIDs; better if you want "user X did Y" narrative logs than field diffs.Both are maintained. This bundle's last release was 2017-08-07 and the repo is archived.
What the bundle's model looks like (and what to copy): one row per changed field-set per flush, with columns roughly action (create/update/remove), objectId, objectClass, version, data (the changed fields), loggedAt, and the added username. Reading history is a query on objectClass + objectId ordered by version DESC; reverting is hydrating an entity from a prior row's data.
Porting the pattern to Laravel:
trait Auditable
{
public static function bootAuditable(): void
{
static::created(fn ($m) => $m->writeAudit('created', [], $m->getAttributes()));
static::updated(fn ($m) => $m->writeAudit('updated', $m->getOriginal(), $m->getChanges()));
static::deleted(fn ($m) => $m->writeAudit('deleted', $m->getOriginal(), []));
}
protected function writeAudit(string $event, array $old, array $new): void
{
$this->audits()->create([
'event' => $event,
'old_values' => $old,
'new_values' => $new,
'user_id' => auth()->id(), // null-safe: console/queue have no session
'url' => request()?->fullUrl(),
'ip_address' => request()?->ip(),
'batch_id' => static::$auditBatchId ??= (string) Str::uuid(),
]);
}
}
Key integration tips that mirror how the bundle is wired:
Auth::id() but allow an explicit override (Auditor::actingAs($userId)) for console commands, queued jobs, and impersonation. Without this you get a table full of null causers from cron.@Loggable annotation; in Laravel do it via the trait. Do not wire a global Model::updated listener — you will capture session rows, pivot churn, and job bookkeeping.subject_type/subject_id index, an index on (causer_type, causer_id, created_at), and a JSON column. The bundle's objectClass/objectId pair is exactly this; keep the composite index or history screens get slow fast.batch_id (Spatie calls it batch_uuid) so a multi-model transaction renders as a single timeline entry.$model->fill($audit->old_values)->saveQuietly() inside a transaction, and write the revert itself as an audit event so the history stays append-only.config/database.php and map an Eloquent model onto log_entry. Consume; don't co-own.symfony/* version conflicts and legacy Doctrine\Common\Persistence interfaces. This is a hard blocker, not a nuisance.onFlush for managed entities; DQL UPDATE/DELETE, ->update() on a query builder, and raw SQL all skip it. Same trap in Laravel with Model::query()->update() (no model events). Document it, or forbid bulk writes on audited tables.data column is often PHP-serialized. If you ever read the legacy table from Laravel, use unserialize($v, ['allowed_classes' => false]) or migrate the column to JSON first. Never unserialize() untrusted rows.username lands as null — indistinguishable from an anonymous web edit. This is the bundle's weakest point and the thing to fix first in a port.audits:prune --days=365) or the table outgrows the app within a year on a busy model.@Versioned; in Laravel use $model->getHidden() or an explicit $auditExclude array. Password hashes, tokens, and PII must never land in the diff column.@Loggable, and the log table exists — in that order. The Laravel analogue is: trait booted (bootAuditable naming matters), events not suppressed (saveQuietly, withoutEvents), and the migration ran.gedmo.listener.loggable.owen-it/laravel-auditing if you need field-level diffs with a causer, spatie/laravel-activitylog if you need event timelines, and steal this bundle's one good idea (causer resolved at flush time, overridable per execution context) wholesale.How can I help you explore Laravel packages today?