In a legacy project, we wanted to record every change made to various models. Storing these “snapshots” allows us to keep a history of how data evolves. However, the existing code wasn’t completed, so we created a simple trait to demonstrate an easy way to implement it without significantly altering the existing codebase.
The Mirror Trait
Here’s the “Mirror” trait, which takes care of saving a snapshot each time the model is saved, updated, or deleted:
<?php
namespace App\Traits;
use App\Models\Mirror as Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
trait Mirror
{
    public function mirrors(): MorphMany
    {
        return $this->morphMany(Model::class, 'mirrored');
    }
    protected function makeMirror()
    {
        $this->mirrors()->create([
            'lock_version' => $this->lock_version,
            'etag' => $this->etag,
            'data' => $this->toArray(),
            // Optionally, we will store who took the action if available.
            'causer_id' => Auth::user()?->id,
            'causer_type' => Auth::user()?->getMorphClass(),
        ]);
    }
    public static function bootMirror()
    {
        static::saving(function ($model) {
            $model->lock_version = $model->lock_version ? ($model->lock_version + 1) : 1;
            $model->etag = Str::uuid()->toString();
        });
        static::created(function ($model) {
            $model->makeMirror();
        });
        static::updated(function ($model) {
            $model->makeMirror();
        });
        static::deleted(function ($model) {
            $model->makeMirror();
        });
    }
}The Mirrors Table
In your database, create a table to store these snapshots. Here’s an example schema:
CREATE TABLE IF NOT EXISTS "mirrors" (
    // ...
    "mirrored_type" VARCHAR NOT NULL,
    "mirrored_id" INTEGER NOT NULL,
    "causer_type" VARCHAR,    // optional
    "causer_id" INTEGER,      // optional
    "lock_version" INTEGER NOT NULL,
    "etag" VARCHAR NOT NULL,
    "data" TEXT NOT NULL,     // could also be JSON
    // ...
);The Mirror Model
The Mirror model itself looks like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Mirror extends Model
{
    protected $guarded = [];
    protected $casts = [
        'data' => 'array',
    ];
    public function mirrored(): MorphTo
    {
        return $this->morphTo();
    }
}Using the Mirror Trait in an Model
For example: we are using Order model. Let’s say we have an orders table:
CREATE TABLE IF NOT EXISTS "orders" (
    // ...
    "lock_version" INTEGER NOT NULL,
    "etag" VARCHAR NOT NULL,
    // ...
);We then create our Order model and include the Mirror trait:
<?php
namespace App\Models;
use App\Traits\Mirror;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
    use Mirror;
    protected $guarded = [];
}Testing It Out
Whenever you create or update an Order, a snapshot will automatically be saved in the mirrors table. Here’s an example:
// Creating a new record
\App\Models\Order::create([
  'total' => 100,
  'items' => [
    ['title' => 'WordPress Hat', 'price' => 25, 'quantity' => 2],
    ['title' => 'WordPress T-Shirt', 'price' => 50, 'quantity' => 1],
  ]
]);
// Updating an existing record
\App\Models\Order::first()->update(['total' => 90]);Both actions will automatically update lock_version, generate a new etag, and create a record of these changes in mirrors.
Limitations
Note that changes only occur when the model’s save, create, or update methods are invoked. Mass operations bypass these model events:
// This won't create snapshots or update lock_version/etag:
\App\Models\Order::query()->update(['total' => 10]);
// This won't work either:
\App\Models\Order::insert([
  // ...
]);
// But iterating through each record does work:
\App\Models\Order::query()->each(fn ($order) => $order->update(['total' => 10]));Sample Queries & Output:

Conclusion
This simple approach should work for most applications. Still, feel free to adapt it to your specific needs. By using a polymorphic relationship, automatically saving model snapshots whenever a record is created, updated, or deleted, you can easily track data changes without significantly modifying your existing code.

Leave a Reply