ss-ipg/laravel-auditable
Attribute-based audit logging for Laravel Eloquent models. Add #[Auditable] to track create/update/delete/soft delete/restore events with old/new values, column include/exclude/redact, per-model event filters, JSON formatting, and extensible context providers.
Installation: Add the package via Composer:
composer require ss-ipg/laravel-auditable
Publish the config:
php artisan vendor:publish --tag=auditable-config
Configure Logging: Add an audit channel to config/logging.php:
'audit' => [
'driver' => 'daily',
'path' => storage_path('logs/audit.log'),
'level' => 'info',
'days' => 14,
],
Enable Auditing: Set AUDITABLE_ENABLED=true in .env and configure environments in config/auditable.php.
First Use Case: Mark a model with #[Auditable] to start logging all CRUD operations:
use SSIPG\Auditable\Attributes\Auditable;
#[Auditable]
class User extends Model {}
Now, every create, update, delete, etc., on User will be logged automatically.
Declarative Setup: Use #[Auditable] on models to enable auditing with zero boilerplate.
#[Auditable(columns: ['name', 'email'], redact: ['password'])]
class User extends Model {}
Granular Control: Leverage attribute options for fine-tuned auditing:
events: [AuditAction::Deleted]).redact: ['api_key']).Context Enrichment: Add custom metadata via AuditContextProvider:
class TenantContextProvider implements AuditContextProvider {
public function getContext(Model $model, AuditAction $action): array {
return ['tenant_id' => $model->tenant_id];
}
}
Register in config/auditable.php:
'context_providers' => [TenantContextProvider::class],
Testing: Use Audit::fake() to verify auditing in tests:
public function test_user_update_is_audited() {
Audit::fake();
$user = User::find(1);
$user->update(['email' => 'new@example.com']);
Audit::assertLogged(AuditAction::Updated);
}
SoftDeletes trait and logs soft_deleted/restored events.Pivot model with #[Auditable] for relationship auditing.Model::update() or insert()—always use model instances for audited changes.events: [].Compliance Models: Audit all events for critical data:
#[Auditable(events: [AuditAction::Created, AuditAction::Updated, AuditAction::Deleted])]
class PatientRecord extends Model {}
High-Volume Models: Reduce log size by excluding original values:
#[Auditable(withOriginal: false)]
class Comment extends Model {}
Multi-Tenant Apps: Add tenant context to every log entry:
class TenantContextProvider implements AuditContextProvider {
public function getContext(Model $model, AuditAction $action): array {
return ['tenant_id' => auth()->user()->tenant_id];
}
}
Mass Operations Bypass Auditing:
Model::update(), Model::insert(), or query builder operations (e.g., DB::table()->update()) do not trigger Eloquent events.// ✅ Audited
$user->update(['email' => 'new@example.com']);
// ❌ NOT audited
User::where('id', 1)->update(['email' => 'new@example.com']);
Timestamp Columns Excluded:
created_at, updated_at, and deleted_at are automatically excluded from change tracking (the audit log has its own timestamp).Attribute Conflicts:
#[Attribute] for other purposes, ensure namespace imports are correct:
use SSIPG\Auditable\Attributes\Auditable; // Correct
// Avoid:
use Attribute; // PHP 8.1+ built-in
Soft Deletes Misconfiguration:
SoftDeletes is not detected, verify the trait is properly imported:
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model {
use SoftDeletes;
}
Log Format Issues:
formatter in config/auditable.php. Default is JsonFormatter, but custom formatters must implement AuditFormatter.Missing Logs:
AUDITABLE_ENABLED=true and the environment is in config/auditable.environments.audit) exists in config/logging.php.Testing Quirks:
Audit::fake() does not log to files—it captures entries in memory. Reset with Audit::flush() between tests.Audit::logged() to inspect captured entries:
$entries = Audit::logged(AuditAction::Updated);
dd($entries);
Custom Formatters:
AuditFormatter:
class CustomFormatter implements AuditFormatter {
public function format(array $payload): string {
return "[AUDIT] " . json_encode($payload, JSON_PRETTY_PRINT);
}
}
config/auditable.php:
'formatter' => App\Audit\CustomFormatter::class,
Dynamic Context:
class RequestContextProvider implements AuditContextProvider {
public function getContext(Model $model, AuditAction $action): array {
return [
'request_id' => request()->header('X-Request-ID'),
'user_agent' => request()->userAgent(),
];
}
}
Event Filtering:
#[Auditable(events: fn(Model $model) => [
AuditAction::Created,
$model->is_audit_critical ? AuditAction::Updated : [],
])]
class Product extends Model {}
AUDITABLE_ENABLED=false in .env for local/dev environments.config(['auditable.enabled' => false]);
User::where(...)->update(...);
config(['auditable.enabled' => true]);
audit channel in config/logging.php to manage log file size/days.Conditional Auditing: Use a closure for dynamic event filtering:
#[Auditable(events: fn(Model $model) => [
AuditAction::Created,
$model->is_sensitive ? AuditAction::Updated : [],
])]
class Document extends Model {}
Cross-Model Auditing:
Audit changes to related models via observers or model events:
class OrderObserver {
public function saved(Order $order) {
if ($order->wasChanged('status')) {
$order->auditLog()->create([...]); // Custom audit table
}
}
}
Audit Trail Queries: Query audit logs directly (if using a database driver):
// Example: Find all updates to a user's email
$auditLogs = AuditLog::where('model_type', User::class)
->where('action', AuditAction::Updated)
->whereJsonContains('changes->email', ['new']);
How can I help you explore Laravel packages today?