What's new in laravel-activitylog v5 original
We just released v5 of laravel-activitylog, our package for logging user activity and model events in Laravel.
In Flare, Mailcoach, and Oh Dear we use it to build audit logs, so we can track what users are doing: who changed a setting, who deleted a project, who invited a team member. If you need something similar in your app, this package makes it easy.
This major release requires PHP 8.4+ and Laravel 12+, and brings a cleaner API, a better database schema, and customizable internals. Let me walk you through what the package can do and what's new in v5.
Using the package
At its core, the package lets you log what happens in your application. The simplest usage looks like this:
activity()->log('Look mum, I logged something');
You can retrieve all logged activities using the Activity model:
use Spatie\Activitylog\Models\Activity; $lastActivity = Activity::all()->last(); // returns 'Look mum, I logged something' $lastActivity->description;
Most of the time you want to know two things: what was affected, and who did it. The package calls these the subject and the causer. You can also attach custom properties for any extra context you need:
activity() ->performedOn($article) ->causedBy($user) ->withProperties(['via' => 'admin-panel']) ->log('edited'); $lastActivity = Activity::all()->last(); // the article that was edited $lastActivity->subject; // the user who edited it $lastActivity->causer; // 'admin-panel' $lastActivity->getProperty('via');
The Activity model provides query scopes to filter your activity log:
Activity::forSubject($newsItem)->get(); Activity::causedBy($user)->get(); Activity::forEvent('updated')->get(); Activity::inLog('payment')->get(); // or combine them Activity::forSubject($newsItem) ->causedBy($user) ->forEvent('updated') ->get();
Automatic model event logging
Imagine you want to track whenever a model is created, updated, or deleted. Just add the LogsActivity trait to your model. In v5, that's all you need for basic logging:
use Illuminate\Database\Eloquent\Model; use Spatie\Activitylog\Models\Concerns\LogsActivity; class NewsItem extends Model { use LogsActivity; }
That's it. No getActivitylogOptions() method needed. Now imagine you also want to track which attributes changed. Override the method and tell it what to watch:
use Spatie\Activitylog\Support\LogOptions; class NewsItem extends Model { use LogsActivity; protected $fillable = ['name', 'text']; public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() ->logOnly(['name', 'text']); } }
Now when you update a news item, the package tracks exactly what changed:
$newsItem->name = 'updated name'; $newsItem->save(); $activity = Activity::all()->last(); $activity->attribute_changes; // [ // 'attributes' => [ // 'name' => 'updated name', // 'text' => 'Lorem', // ], // 'old' => [ // 'name' => 'original name', // 'text' => 'Lorem', // ], // ]
You can log all fillable attributes with logFillable(), all unguarded attributes with logUnguarded(), or use logAll() combined with logExcept() to log everything except sensitive fields like passwords.
If you only want to see what actually changed rather than all tracked attributes, chain logOnlyDirty().
Running code before an activity is saved
When a model event is logged, you can hook into the process by defining a beforeActivityLogged() method on your model:
class NewsItem extends Model { use LogsActivity; public function beforeActivityLogged(Activity $activity, string $eventName): void { $activity->properties = $activity->properties->merge([ 'ip_address' => request()->ip(), ]); } }
This runs right before the activity is persisted, giving you a chance to enrich it with extra data.
Customizable action classes
The core operations of the package (logging activities and cleaning old records) are now handled by action classes. You can extend these and swap them in via config.
For example, say you want to save activities to the queue instead of writing them to the database during the request. Extend the action and override the save() method:
use Spatie\Activitylog\Actions\LogActivityAction; class QueuedLogAction extends LogActivityAction { protected function save(Model $activity): void { dispatch(new SaveActivityJob($activity->toArray())); } }
Then tell the package to use your custom action in config/activitylog.php:
'actions' => [ 'log_activity' => QueuedLogAction::class, ],
You can also override transformChanges() to manipulate the changes array before saving. Here's an example that redacts password changes so they never end up in your activity log:
use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Spatie\Activitylog\Actions\LogActivityAction; class RedactSensitiveFieldsAction extends LogActivityAction { protected function transformChanges(Model $activity): void { $changes = $activity->attribute_changes?->toArray() ?? []; Arr::forget($changes, ['attributes.password', 'old.password']); $activity->attribute_changes = collect($changes); } }
Buffering activities
Say you have an endpoint that updates product prices in bulk. Each product update triggers a model event, and each model event logs an activity. With 200 products, that's 200 INSERT queries just for activity logging.
foreach ($products as $product) { // each update triggers an activity INSERT query $product->update(['price' => $newPrices[$product->id]]); }
With buffering enabled, the package collects all those activities in memory during the request and inserts them in a single bulk query after the response has been sent to the client. Your user gets a fast response, and the database does one insert instead of 200.
Buffering is off by default. You can enable it in the config/activitylog.php config file. No other code changes needed. All existing logging code (both automatic model event logging and manual activity()->log() calls) will be buffered automatically.
Under the hood, the ActivityBuffer class collects activities in an array and inserts them all at once when flushed:
class ActivityBuffer { protected array $pending = []; public function add(Model $activity): void { $this->pending[] = $this->prepareForInsert($activity); } public function flush(): void { if (empty($this->pending)) { return; } $modelClass = Config::activityModel(); $modelClass::query()->insert($this->pending); $this->pending = []; } }
The service provider takes care of flushing the buffer at the right time:
protected function registerActivityBufferFlushing(): void { // flush after the response has been sent $this->app->terminating(fn () => app(ActivityBuffer::class)->flush()); // flush after each queued job $this->app['events']->listen( [JobProcessed::class, JobFailed::class], fn () => app(ActivityBuffer::class)->flush(), ); // safety net if the application terminates unexpectedly register_shutdown_function(function () { try { app(ActivityBuffer::class)->flush(); } catch (\Throwable) { } }); }
One thing to be aware of: buffered activities won't have a database ID until the buffer is flushed. If you need to read back the activity ID immediately after logging, don't enable buffering.
This works with Octane (the buffer is a scoped binding, so it resets between requests) and queues out of the box.
In closing
v5 doesn't bring a lot of new features, but it modernizes the package, cleans up the internals, and makes the things that were hard to customize in v4 easy to swap out. If you're upgrading from v4, be aware that there are quite a few breaking changes. Check the upgrade guide for the full list.
You can find the complete documentation at spatie.be/docs/laravel-activitylog and the source code on GitHub.
This is one of the many packages we've created at Spatie. If you want to support our open source work, consider picking up one of our paid products.