<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('audits', function (Blueprint $table) {
$table->id();
$table->string('event', 32);
$table->morphs('auditable');
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamps();
$table->index(['auditable_type', 'auditable_id']);
});
}
public function down(): void
{
Schema::dropIfExists('audits');
}
};
<?php
namespace App\Models\Concerns;
use App\Models\Audit;
use App\Observers\AuditObserver;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait Auditable
{
public static function bootAuditable(): void
{
static::observe(AuditObserver::class);
}
public function audits(): MorphMany
{
return $this->morphMany(Audit::class, 'auditable')->latest();
}
public function auditExclude(): array
{
return array_merge(
['updated_at', 'created_at', 'remember_token', 'password'],
property_exists($this, 'auditExclude') ? $this->auditExclude : []
);
}
}
<?php
namespace App\Observers;
use App\Models\Audit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
class AuditObserver
{
public function created(Model $model): void
{
$this->writeAudit($model, 'created', [], $this->filter($model, $model->getAttributes()));
}
public function updated(Model $model): void
{
$changed = $this->filter($model, $model->getDirty());
if (empty($changed)) {
return; // no meaningful change; skip a noisy row
}
$old = [];
foreach (array_keys($changed) as $key) {
$old[$key] = $model->getOriginal($key);
}
$this->writeAudit($model, 'updated', $old, $changed);
}
public function deleted(Model $model): void
{
$this->writeAudit($model, 'deleted', $this->filter($model, $model->getOriginal()), []);
}
protected function filter(Model $model, array $values): array
{
return array_diff_key($values, array_flip($model->auditExclude()));
}
protected function writeAudit(Model $model, string $event, array $old, array $new): void
{
Audit::withoutEvents(function () use ($model, $event, $old, $new) {
Audit::create([
'event' => $event,
'auditable_type' => $model->getMorphClass(),
'auditable_id' => $model->getKey(),
'user_id' => Auth::id(),
'old_values' => $old,
'new_values' => $new,
'ip_address' => Request::ip(),
'user_agent' => substr((string) Request::userAgent(), 0, 255),
]);
});
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Audit extends Model
{
protected $fillable = [
'event', 'auditable_type', 'auditable_id',
'user_id', 'old_values', 'new_values',
'ip_address', 'user_agent',
];
protected $casts = [
'old_values' => 'array',
'new_values' => 'array',
];
public function auditable(): MorphTo
{
return $this->morphTo();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
This snippet shows how a Laravel application records a durable audit trail of Eloquent model changes using an observer and a small reusable trait. The core idea is to keep a history of who changed what and when, without scattering logging calls through controllers and services. Because Eloquent fires lifecycle events (created, updated, deleted), an observer can hook into those events centrally and write an Audit row that captures the diff.
In audits table migration, the schema is designed as an append-only log: auditable_type and auditable_id form a polymorphic reference back to the changed model, event records the lifecycle action, and old_values/new_values are JSON columns holding just the attributes that actually changed. Storing the diff rather than a full snapshot keeps rows compact and makes each entry self-explanatory. The user_id column records the actor, and an index on the polymorphic pair keeps per-record history queries fast.
Auditable trait provides the glue. bootAuditable registers AuditObserver on any model that uses the trait, so wiring is a single use Auditable; line per model. The audits method exposes a morphMany relation so history is queryable directly from the record. auditExclude lets a model hide noisy fields like updated_at or remember_token from the log.
AuditObserver does the real work. On updated, it calls getDirty() to find changed attributes and getOriginal() to recover their prior values, filters out excluded keys, and skips writing entirely when nothing meaningful changed — an important guard, since Eloquent can fire updated even on a no-op save. The writeAudit helper resolves the acting user via Auth::id() and captures request metadata like IP and user agent. Using withoutEvents when persisting the Audit avoids any risk of recursive auditing.
The trade-off is that this approach only captures changes made through Eloquent; raw query-builder updates bypass model events and won't be logged. For most compliance and debugging needs, though, this pattern gives a clean, centralized, low-friction audit trail that travels with each model.
Related snips
import os
import stat
for root, _dirs, files in os.walk('/etc'):
for name in files:
path = os.path.join(root, name)
Python security audit script for exposed risky filesystem state
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
Laravel mix/Vite for asset compilation
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Laravel database migrations for schema management
#!/usr/bin/env bash
set -euo pipefail
find / -perm -4000 -type f 2>/dev/null | sort
sudo -l
find /etc/systemd/system -type f -writable 2>/dev/null
Linux privilege escalation checks for suspicious local state
Share this code
Here's the card — post it anywhere.