alama@world: ~/
./toggle-theme
cat blog/oss-adrs-laravel-activitylog.md

OSS-ADR: Spatie Laravel Activitylog

· post · energy

Deep dive into the architecture of Spatie's Laravel Activitylog, focusing on Bootable Traits and Eloquent casting serialization.

1. Context & Decision

The Problem: In almost any application with multiple users, you will inevitably receive the dreaded question: "Who changed this setting, and when did they do it?" Tracking changes in Eloquent models over time (an audit trail) is essential for debugging, compliance, and user accountability.

The Alternatives: You could implement custom Eloquent Observers for every model, capturing $model->getDirty() and storing it in a generic audits table. Alternatively, there are other packages like owen-it/laravel-auditing which offer similar functionality but have a steeper learning curve and different architectural opinions.

The Decision: We chose spatie/laravel-activitylog because of its outstanding Developer Experience (DX), its robust handling of attribute serialization, and Spatie's track record of maintaining high-quality, clean code. It seamlessly fits into the Laravel ecosystem and provides a fluent API for logging both model changes and custom events.

2. The Integration

The Quick Win: Getting started is unbelievably simple. You just add the LogsActivity trait and define your options on the model:

use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['name', 'text']) // Only log these attributes
            ->logOnlyDirty(); // Only create a log when these actually change
    }
}

Now, whenever an Article is created or updated, a record is automatically saved.

Practical Usage: To view the logs, you utilize the polymorphic relation provided by the trait:

$article = Article::find(1);
$activities = $article->activities; // Returns a collection of Spatie\Activitylog\Models\Activity

$lastActivity = $activities->last();
echo $lastActivity->causer->name; // "Mohammed"
print_r($lastActivity->changes()->toArray()); 
// Outputs the 'old' and 'attributes' (new) values!

3. Under the Hood

Architecture & Design Patterns: Instead of forcing you to register traditional Laravel Observer classes (which can be easily forgotten in a ServiceProvider), Spatie uses the Bootable Trait pattern.

Inside the LogsActivity trait, there is a bootLogsActivity() method. Laravel's Eloquent automatically looks for methods matching boot{TraitName} during model boot. Inside this method, Spatie loops through the model's events (created, updated, deleted) and dynamically binds closures to them:

static::$eventName(function (Model $model) use ($eventName) {
    // ... logic to build changes
    app(ActivityLogger::class)
        ->useLog($model->getLogNameToUse())
        ->event($eventName)
        ->performedOn($model)
        ->withChanges($changes)
        ->log($description);
});

This guarantees that if the trait is there, the listener is there. No extra configuration required. It uses a Polymorphic One-to-Many relationship (morphMany) to link the central Activity model to any subject (performedOn) and any causer (causedBy).

Code Masterclass: Handling Casts and Serialization One of the most annoying parts of building an audit logger from scratch is dealing with Eloquent casts (like Enums or custom DateTimes). If an attribute is cast to an Enum, you can't just blindly json_encode it.

Spatie elegantly handles this in their extractChanges and formatAttributeValue methods within the trait. They actively check Eloquent's internal casting mechanisms before storing the data:

protected static function formatAttributeValue(Model $model, string $attribute, mixed $value): mixed
{
    if ($model->hasCast($attribute)) {
        $cast = $model->getCasts()[$attribute];

        if ($model->isEnumCastable($attribute)) {
            return $model->getStorableEnumValue($cast, $value);
        }
        // ... handles DateTime casts and Date serialization
    }
    return $value;
}

By hooking directly into Eloquent's internal casting engine (isEnumCastable, getStorableEnumValue), the package ensures that what gets stored in the activity log perfectly mirrors what would be stored in the primary database table.

4. Consequences & Trade-offs

The Good: - Extensibility: The configuration via LogOptions is incredibly fluent and highly customizable per model. - Maintenance: It abstracts away the heavy lifting of dirty-checking and type serialization.

The Gotchas: - Database Growth: Activity logs grow exponentially in busy applications. You must implement a pruning strategy (which the package offers via an artisan command) or your database will balloon in size. - Performance Overhead: Because it hooks into Eloquent's updating and updated events, running mass updates via Eloquent loops can cause N+1 insert queries for the activity logs. (Note: Mass updates via $query->update() bypass Eloquent events entirely, meaning they won't be logged unless you handle them manually).

#oss #architecture #spatie #laravel