laravel-enso/activity-log
Laravel Enso Activity Log adds an audit trail to your application, recording user actions and model changes so administrators can review who did what and when. It integrates with Enso’s UI and permissions for straightforward activity monitoring.
This is an Enso-ecosystem package: it is designed to be dropped into an application that already runs laravel-enso/core, not into a bare Laravel app. Before you install anything, decide whether that constraint is acceptable — the package is archived, has essentially no independent adoption, and its "beautiful to look at" value comes from the companion enso-ui/activity-log frontend, not from the PHP side. If you are not on Enso, spatie/laravel-activitylog (which this package's storage shape closely mirrors) or owen-it/laravel-auditing will cost you less.
If you are on Enso, the path is short:
composer require laravel-enso/activity-log and run php artisan migrate — the package ships the activity_log table (log_name, description, subject_type/subject_id, causer_type/causer_id, properties, timestamps).php artisan vendor:publish --tag=activity-log-config (verify the exact tag in src/Providers — with 2 stars, assume nothing about tag names and confirm against the source).src/Traits/ first. That is where the model-side behaviour lives; the trait name and its configurable properties ($logEvents, log name, attributes-to-log) are the whole API surface you will touch day to day.User, Company, Invoice) and edit a record in the UI — you should see a created/updated row with old and new values in properties.Trait-per-model, configured per-model. The idiomatic usage is to put the logging trait on each model you care about and narrow it there: only log created/updated/deleted, and only the attributes that matter. Logging every column on a wide model makes the history panel unreadable and the table enormous.
Read through the Activity model, not raw SQL. Treat it as a normal Eloquent model: subject() and causer() are morphTo relations, and properties is a JSON cast. Build your own timelines with Activity::where('subject_type', Post::class)->where('subject_id', $id)->latest()->get(), and eager-load causer so the history list doesn't N+1.
Mirror the Enso patterns when customising. Where the package supports manual entries (the underlying API accepts a performed-on subject, a causer, and a properties array), use that for business events that aren't model saves — "invoice marked paid", "export generated" — so the audit trail reads as one story rather than two.
Hook it into the edit screen. The natural integration point is a read-only history tab/panel on the record's detail page, fed by the morph relations. Keep it a display concern: don't let the activity log drive application state.
Test it at the boundary. Because logging happens via model events, a feature test that updates a record and then asserts an activity_log row exists (with the expected description and properties keys) is far more valuable than unit-testing the trait. Also add a test that an unauthorised update produces no log.
Bulk operations need deliberate handling. Because the mechanism is Eloquent events, Model::query()->update([...]) and insert() bypass it entirely. If you need those tracked, log manually around the query, or convert to per-model saves where volume permits.
composer.json and expect PHP/Laravel constraint conflicts on modern releases. Do not plan on upstream fixes; be prepared to fork and patch the trait yourself.enso-ui/*. Wiring this into a non-Enso app means keeping the PHP logging and discarding the UI — at which point you should question the dependency.withoutEvents(), queue workers using DB::table() — silently leaves no trace. This is the single most common source of "why isn't this showing up?"subject_type stores the FQCN unless you use a morph map. Rename or namespace a model and every historical row stops resolving. Introduce a morph map before you accumulate history.causer is null in console and most queued jobs. If your audit story requires "who", pass the causer explicitly in job payloads rather than relying on auth().activity_log becomes one of the largest tables in an active app. Add composite indexes for your actual query shape (subject_type, subject_id, created_at), and schedule pruning of rows older than your retention window from day one.updated_at-only writes and from save loops (e.g. a saved observer that touches a parent). Diff getChanges() mentally when reading logs, and scope the logged attributes down until the signal-to-noise ratio is acceptable.withoutEvents) and confirm the row count stops changing — it isolates package behaviour from your own observers fast.How can I help you explore Laravel packages today?