How to record the history of any model with Laravel observers to Sentry
A few weeks ago, I had a client who wanted to track changes made to certain database records and had made them. He asked for a written history of records in the specified tables. Laravel makes this easy with model observers and polymorphic relations.
Read more: How to record the history of any model with Laravel observers to SentryFirstly, you must register an account on the Sentry website or use self-hosted Sentry and create a project. Creating an account and project in Sentry is very easy, and we will skip this process in our blog post. Once you have done that, come back here!
Secondly, let’s set up our activity_log observer.
Laravel model observers work by hooking into model events and allowing you to observe them well. You can alter the model before continuing, cancel an event that is currently happening, or, in my case, handle an action after an event was fired.
I wanted to be able to implement this very uniformly for any model that needed an activity log. Ideally, I wanted something like this.
<?php
public function created($model)
{
$this->track($model);
}
I made something like this and also allowed for customization when necessary. I created a trait so that it would be easily reusable across various observers:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Log;
trait TracksActivityLogTrait
{
protected function track(Model $model, callable $function = null, $table = null, $id = null): void
{
// Allow for overriding of table if it's not the model table.
$table = $table ?: $model->getTable();
// Allow for overriding of id if it's not the model id.
$id = $id ?: $model->id;
// Allow for customization of the history record if needed.
$function = $function ?: [$this, 'getHistoryBody'];
$modelName = (new \ReflectionClass($model))->getShortName();
// Get the dirty fields and run them through the custom function, then insert them into the activity log.
$this->getCreated($model)
->map(function ($value, $field) use ($function) {
return call_user_func_array($function, ['Updated', $value, $field]);
})
->each(function ($fields) use ($modelName, $table, $id) {
Log::info("Log activity log of the {$modelName} model", [
'reference_table' => $table,
'reference_id' => $id,
'user_id' => Auth::user()->id,
'fields' => $fields,
]);
});
}
protected function getHistoryBody(string $action, string $value, string $field): array
{
return [
'body' => "{$action} {$field} to ${value}",
];
}
protected function getCreated(Model $model): Collection
{
return collect($model->getDirty());
}
}
<?php
namespace App\Observers;
use App\Traits\TracksActivityLogTrait;
use Illuminate\Database\Eloquent\Model;
class MyModelObserver
{
use TracksActivityLogTrait;
public function created(Model $model): void
{
$this->track($model);
}
}
Thirdly, register our observer.
The observer can be registered using the boot method of one of the service providers. In our scenario, we register the observer in the AppServiceProvider.
<?php
namespace App\Providers;
use App\Observers\MyModelObserver;
use Illuminate\Support\Facades\File;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
...
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$availableModels = [];
foreach (File::allFiles(app_path('Models')) as $modelFile) {
$availableModels[] = '\App\\Models\\' . $modelFile->getFilenameWithoutExtension();
}
foreach ($availableModels as $availableModel) {
$availableModel::observe(MyModelObserver::class);
}
}
}
And there you have it! Painless record activity log tracking with Laravel.