1. Go to this page and download the library: Download williamug/audited library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
williamug / audited example snippets
// config/audit.php
return [
// The Eloquent model used to store log entries.
// Swap this for your own model to add extra relationships or columns.
'model' => \Williamug\Audited\Models\AuditLog::class,
// Your application's User model.
'user_model' => \App\Models\User::class,
// The field on your User model used as the display name in log entries.
'user_name_field' => 'name',
// Optional: a role or level field on your User model (e.g. 'role', 'level').
// Set to null if your app does not have this concept.
'user_level_field' => null,
// Automatically log Login, Logout, and Failed auth events.
'log_auth_events' => true,
// The module label written to auth event log entries.
'auth_module' => 'Authentication',
// Fields stripped from old_values / new_values before saving.
'sensitive_fields' => [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
],
// The credential field used to identify the subject in failed login entries.
// Common values: 'email', 'username', 'phone_number'.
'login_credential_field' => 'email',
// Logs older than this many months are removed by audit:prune.
// Set to null to disable automatic pruning.
'prune_after_months' => 3,
// The database table name.
'table' => 'audit_logs',
// Set to false (default) to write audit logs synchronously.
// Set to true to dispatch on the default queue.
// Set to a queue name string (e.g. 'audit') to use a specific queue.
'queue' => env('AUDIT_QUEUE', false),
// When true, exceptions thrown during a log write are swallowed and sent
// to Laravel's logger instead of bubbling up to the caller.
'silent_failures' => env('AUDIT_SILENT_FAILURES', false),
];
use Williamug\Audited\Traits\Auditable;
class Invoice extends Model
{
use Auditable;
// The module label written to log entries for this model.
// Defaults to the class base name ('Invoice') if omitted.
protected string $auditModule = 'Billing';
}
class Invoice extends Model
{
use Auditable;
protected string $auditModule = 'Billing';
public function auditLabel(): string
{
return "Invoice #{$this->invoice_number} ({$this->client_name})";
}
}
class User extends Model
{
use Auditable;
protected string $auditModule = 'Users';
// These fields will never appear in old_values or new_values for this model.
public array $auditExclude = ['last_seen_at', 'login_count', 'api_token'];
}
audited(AuditAction::Approve, 'Collections', 'Approved Sunday collection.');
audited('transfer', 'Ministers', 'Transferred Rev. John to St. Peters Parish');
use Williamug\Audited\Services\ActivityLogService;
ActivityLogService::log(AuditAction::Approve, 'Collections', 'Approved Sunday collection.');
audited('transfer', 'Ministers', 'Transferred Rev. John to St. Peters Parish');
audited('ordination', 'Ministers', 'Rev. James ordained as Deacon.');
audited('suspension', 'Staff', 'Staff member suspended pending investigation.');
audited('reconcile', 'Accounts', 'Monthly accounts reconciled.');
// Attribute to a specific user (e.g. admin acting on behalf of another user)
Audited::log(
AuditAction::Create,
'Accounts',
"Admin created account for '{$newUser->name}'.",
causer: $adminUser,
);
// No log entries are written for Invoice inside this callback
Invoice::withoutAudit(function () use ($invoices) {
foreach ($invoices as $data) {
Invoice::create($data);
}
});
Invoice::withoutAudit(function () {
Invoice::create($data); // not logged
AuditLog::forceCreate($row); // unaffected
Payment::create($payData); // logged normally if Payment uses Auditable
});
use Williamug\Audited\Causers\SystemCauser;
use Williamug\Audited\Enums\AuditAction;
use Williamug\Audited\Services\ActivityLogService;
// Inside a queued job
ActivityLogService::log(
AuditAction::Create,
'Members',
'Imported 1,500 member records from CSV.',
causer: new SystemCauser('ImportMembersJob', 'job'),
);
// Inside an Artisan command
ActivityLogService::log(
AuditAction::Delete,
'Sessions',
'Purged 230 expired sessions.',
causer: new SystemCauser('sessions:prune', 'command'),
);
// Inside a scheduled task
ActivityLogService::log(
AuditAction::Export,
'Reports',
'Generated nightly financial summary.',
causer: new SystemCauser('NightlyReportGenerator', 'system'),
);
use Williamug\Audited\Contracts\Causer;
class DataMigration implements Causer
{
public function getCauserName(): string { return 'DataMigration v2.3'; }
public function getCauserType(): string { return 'migration'; }
}
ActivityLogService::log(
AuditAction::Update,
'Schema',
'Backfilled missing invoice_number values.',
causer: new DataMigration(),
);
use Illuminate\Database\Eloquent\SoftDeletes;
use Williamug\Audited\Traits\Auditable;
class Invoice extends Model
{
use Auditable, SoftDeletes;
protected string $auditModule = 'Billing';
}
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Williamug\Audited\Traits\Auditable;
class Role extends Model
{
use Auditable;
protected string $auditModule = 'Roles';
// Only relationships listed here will be audited.
public array $auditRelationships = ['permissions'];
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}
}
$role->permissions()->attach([1, 2, 3]);
// Writes:
// action → 'update'
// module → 'Roles'
// description → 'Attached permissions to Role #1'
// old_values → null
// new_values → ['permissions' => [1, 2, 3]]
// subject → Role #1 (polymorphic link)
$role->permissions()->detach([2]);
// Writes:
// action → 'update'
// description → 'Detached permissions from Role #1'
// old_values → ['permissions' => [2]]
// new_values → null
$role->permissions()->sync([3, 4]);
// Role had [1, 2, 3] — writes two entries:
// 1. Detached permissions [1, 2]
// 2. Attached permissions [4]
// (ID 3 was already attached — no entry for it)
public array $auditRelationships = ['permissions', 'roles', 'tags'];
Role::withoutAudit(function () use ($role) {
$role->permissions()->sync([1, 2, 3]); // not logged
});
use Williamug\Audited\Models\AuditLog;
// All logs for a given module, most recent first
AuditLog::forModule('Billing')->latest()->paginate(20);
// All delete actions in the past 30 days
AuditLog::withAction(AuditAction::Delete)
->between(now()->subDays(30), now())
->get();
// All actions performed by a specific user
AuditLog::forUser($user)->latest()->get();
// All log entries for a specific record
AuditLog::forSubject($invoice)->latest()->get();
// Search across description, user name, and IP address
$term = 'John';
AuditLog::query()
->where(function ($q) use ($term) {
$q->where('user_name', 'like', "%{$term}%")
->orWhere('description', 'like', "%{$term}%")
->orWhere('ip_address', 'like', "%{$term}%");
})
->get();
// All audit log entries for this specific invoice
$invoice->auditLogs()->latest()->get();
// Paginate the audit history for a record
$invoice->auditLogs()->latest()->paginate(10);
$log = AuditLog::find($id);
// Returns the Invoice, User, or whatever model was acted on
$log->subject;
use Williamug\Audited\Http\Concerns\ServesAuditLogs;
class AuditLogController extends Controller
{
use ServesAuditLogs;
public function index(Request $request)
{
return Inertia::render('Admin/AuditLog', $this->auditLogProps($request));
}
public function invoiceHistory(Request $request, Invoice $invoice)
{
return Inertia::render('Invoices/History', $this->auditTimelineProps($request, $invoice));
}
}
// app/Models/AuditLog.php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Williamug\Audited\Models\AuditLog as BaseAuditLog;
class AuditLog extends BaseAuditLog
{
public function organisation(): BelongsTo
{
return $this->belongsTo(Organisation::class);
}
}
public function up(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->unsignedBigInteger('organisation_id')->nullable()->index()->after('user_level');
$table->foreign('organisation_id')->references('id')->on('organisations')->nullOnDelete();
});
}
// app/Models/AuditLog.php
namespace App\Models;
use Williamug\Audited\Models\AuditLog as BaseAuditLog;
class AuditLog extends BaseAuditLog
{
protected static function extraColumns(): array
{
return [
'company_id' => auth()->user()?->company_id,
'branch_id' => auth()->user()?->branch_id,
];
}
}
public function up(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->unsignedBigInteger('company_id')->nullable()->index()->after('user_level');
$table->unsignedBigInteger('branch_id')->nullable()->index()->after('company_id');
});
}
class AuditLog extends BaseAuditLog
{
protected static function boot(): void
{
parent::boot();
static::addGlobalScope('tenant', function ($query) {
$query->where('company_id', auth()->user()?->company_id);
});
}
protected static function extraColumns(): array
{
return [
'company_id' => auth()->user()?->company_id,
'branch_id' => auth()->user()?->branch_id,
];
}
}
static::addGlobalScope('tenant', function ($query) {
$user = auth()->user();
$query->where('company_id', $user?->company_id);
if (! $user?->is_head_office) {
$query->where('branch_id', $user?->branch_id);
}
});
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Williamug\Audited\Models\AuditLog as BaseAuditLog;
class AuditLog extends BaseAuditLog
{
protected static function boot(): void
{
parent::boot();
static::addGlobalScope('tenant', function ($query) {
$user = auth()->user();
$query->where('company_id', $user?->company_id);
if (! $user?->is_head_office) {
$query->where('branch_id', $user?->branch_id);
}
});
}
protected static function extraColumns(): array
{
return [
'company_id' => auth()->user()?->company_id,
'branch_id' => auth()->user()?->branch_id,
];
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);
}
}
// config/audit.php
// Dispatch on the default queue
'queue' => env('AUDIT_QUEUE', true),
// Dispatch on a named queue
'queue' => env('AUDIT_QUEUE', 'audit'),
// All audit events from a specific request
AuditLog::where('request_id', $requestId)->get();
// All POST requests to a module in the past week
AuditLog::forModule('Billing')
->where('http_method', 'POST')
->between(now()->subWeek(), now())
->get();
// All actions performed via the API guard
AuditLog::where('auth_guard', 'api')->forUser($user)->get();
public function up(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
// Tags — arbitrary key-value context on manual log entries
$table->json('tags')->nullable()->after('description');
// Request context
$table->string('url', 2048)->nullable()->after('user_agent');
$table->string('http_method', 10)->nullable()->after('url');
$table->string('route_name', 255)->nullable()->index()->after('http_method');
$table->string('auth_guard', 50)->nullable()->after('route_name');
// Polymorphic subject link
$table->string('subject_type', 255)->nullable()->after('auth_guard');
$table->unsignedBigInteger('subject_id')->nullable()->after('subject_type');
$table->index(['subject_type', 'subject_id']);
// Request ID tracing
$table->string('request_id', 36)->nullable()->index()->after('subject_id');
// Causer type — distinguishes human actors from system processes
$table->string('causer_type', 50)->nullable()->after('user_level');
});
}
// config/audit.php
'prune_after_months' => null,
// config/audit.php
'user_name_field' => 'full_name', // or 'email', 'username', etc.
'user_level_field' => 'role', // or 'level', 'tier', etc. Set to null to disable.
// config/audit.php
'sensitive_fields' => [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
'api_key', // add your own
'stripe_secret', // add your own
],
// config/audit.php
'table' => 'activity_logs',
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Williamug\Audited\Traits\Auditable;
class Invoice extends Model
{
use Auditable, SoftDeletes;
// Module label written to every log entry for this model
protected string $auditModule = 'Billing';
// High-frequency columns that are not meaningful audit events
public array $auditExclude = ['last_viewed_at', 'pdf_cached_at'];
// Richer label in log descriptions instead of "Invoice #42"
public function auditLabel(): string
{
return "Invoice #{$this->number} — {$this->client->name}";
}
}
use Williamug\Audited\Enums\AuditAction;
use Williamug\Audited\Services\ActivityLogService;
class InvoiceController extends Controller
{
public function approve(Invoice $invoice): RedirectResponse
{
$invoice->update(['status' => 'approved', 'approved_at' => now()]);
// The update above already writes an automatic log entry.
// This additional manual entry records the business action explicitly.
ActivityLogService::log(
AuditAction::Approve,
'Billing',
"Approved Invoice #{$invoice->number} ({$invoice->client->name}).",
subject: $invoice,
tags: ['approved_by_ip' => request()->ip()],
);
return redirect()->back()->with('success', 'Invoice approved.');
}
}
use Williamug\Audited\Causers\SystemCauser;
class SendOverdueRemindersJob implements ShouldQueue
{
public function handle(): void
{
foreach (Invoice::overdue()->get() as $invoice) {
Mail::to($invoice->client->email)->send(new OverdueReminderMail($invoice));
ActivityLogService::log(
'reminder_sent',
'Billing',
"Overdue reminder sent to {$invoice->client->email}.",
subject: $invoice,
causer: new SystemCauser('SendOverdueRemindersJob', 'job'),
);
}
}
}
use Illuminate\Foundation\Testing\RefreshDatabase;
use Williamug\Audited\Enums\AuditAction;
use Williamug\Audited\Models\AuditLog;
use Williamug\Audited\Services\ActivityLogService;
class InvoiceTest extends TestCase
{
use RefreshDatabase;
public function test_creating_an_invoice_writes_an_audit_log(): void
{
$user = User::factory()->create();
$this->actingAs($user);
Invoice::create(['number' => 'INV-001', 'amount' => 5000]);
$this->assertDatabaseHas('audit_logs', [
'action' => 'create',
'module' => 'Billing',
'user_id' => $user->id,
]);
}
public function test_audit_log_links_to_the_invoice(): void
{
$user = User::factory()->create();
$this->actingAs($user);
$invoice = Invoice::create(['number' => 'INV-001', 'amount' => 5000]);
expect($invoice->auditLogs)->toHaveCount(1)
->and($invoice->auditLogs->first()->action)->toBe('create');
}
public function test_sensitive_fields_are_not_stored(): void
{
$user = User::factory()->create();
$this->actingAs($user);
ActivityLogService::log(
AuditAction::Update,
'Users',
'Updated user.',
['name' => 'Old', 'password' => 'secret'],
['name' => 'New', 'password' => 'newsecret'],
);
$log = AuditLog::latest()->first();
$this->assertArrayNotHasKey('password', $log->old_values);
$this->assertArrayNotHasKey('password', $log->new_values);
}
}