PHP code example of williamug / audited

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::log(AuditAction::Approve, 'Collections', 'Approved Sunday collection.');

// With old/new values
Audited::log(
    AuditAction::Update,
    'Settings',
    'Updated application settings.',
    ['maintenance_mode' => false],
    ['maintenance_mode' => true],
);

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::log(
    action: AuditAction::Create,
    module: 'Members',
    description: 'Imported member records.',
    tags: ['batch_id' => 'imp_2024_001', 'source' => 'csv'],
);

audited(
    action: AuditAction::Update,
    module: 'Settings',
    description: 'Updated fee structure.',
    oldValues: ['base_fee' => 5000],
    newValues: ['base_fee' => 7500],
    tags: ['ticket' => 'SUP-442'],
);

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.');

Audited::log(
    AuditAction::Create,
    'Members',
    'Imported member records.',
    tags: ['batch_id' => 'imp_2024_001', 'source' => 'csv', 'rows' => 1500],
);

audited(
    AuditAction::Update,
    'Settings',
    'Updated fee structure.',
    oldValues: ['base_fee' => 5000],
    newValues: ['base_fee' => 7500],
    tags: ['ticket' => 'SUP-442', 'approved_by' => 'finance'],
);

$log->tags; // ['batch_id' => 'imp_2024_001', 'source' => 'csv', 'rows' => 1500]

Audited::log(
    AuditAction::Approve,
    'Collections',
    'Approved Sunday collection.',
    subject: $collection,
);

// 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(),
);

// config/audit.php
'log_auth_events' => false,

// config/audit.php
'login_credential_field' => 'phone_number',

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\Enums\AuditAction;

AuditAction::Login          // 'login'
AuditAction::Logout         // 'logout'
AuditAction::FailedLogin    // 'failed_login'
AuditAction::PasswordChange // 'password_change'
AuditAction::Create         // 'create'
AuditAction::Update         // 'update'
AuditAction::Delete         // 'delete'
AuditAction::Approve        // 'approve'
AuditAction::Reject         // 'reject'
AuditAction::Export         // 'export'
AuditAction::ViewReport     // 'view_report'
AuditAction::Restore        // 'restore'
AuditAction::ForceDelete    // 'force_delete'

// Human-readable label
AuditAction::PasswordChange->label();  // 'Password Change'
AuditAction::ForceDelete->label();     // 'Force Delete'

// Tailwind CSS badge classes (with dark mode)
AuditAction::Delete->badgeColor();
// 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'

AuditAction::Restore->badgeColor();
// 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400'

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();

// Combine scopes freely
AuditLog::forUser($user)
    ->forModule('Billing')
    ->withAction(AuditAction::Update)
    ->between(now()->startOfMonth(), now()->endOfMonth())
    ->latest()
    ->paginate(20);

// 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;

'api_middleware' => ['web', 'auth:sanctum'],
'api_prefix'     => 'admin/api/audit',     // optional custom prefix

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);
    }
}

// config/audit.php
'model' => \App\Models\AuditLog::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
'model' => \App\Models\AuditLog::class,

// config/audit.php

// Dispatch on the default queue
'queue' => env('AUDIT_QUEUE', true),

// Dispatch on a named queue
'queue' => env('AUDIT_QUEUE', 'audit'),

// config/audit.php
'silent_failures' => env('AUDIT_SILENT_FAILURES', true),

// 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();

AuditLog::where('route_name', 'invoices.update')->latest()->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
'login_credential_field' => 'phone_number',

// 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);
    }
}

Invoice::withoutAudit(function () {
    Invoice::factory()->count(10)->create();
});

use Illuminate\Support\Facades\Queue;
use Williamug\Audited\Jobs\WriteAuditLog;

Queue::fake();
config(['audit.queue' => true]);

Invoice::create(['number' => 'INV-001', 'amount' => 5000]);

Queue::assertPushed(WriteAuditLog::class);
bash
php artisan audit:install
bash
php artisan migrate
blade
@php
  $action = \Williamug\Audited\Enums\AuditAction::tryFrom($log->action);
@endphp

<span class="px-2 py-1 rounded text-xs font-medium
    {{ $action?->badgeColor() ?? 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' }}">
    {{ $action?->label() ?? ucfirst(str_replace('_', ' ', $log->action)) }}
</span>
blade
@foreach ($logs as $log)
    @php
      $action = Williamug\Audited\Enums\AuditAction::tryFrom($log->action);
    @endphp

    <span class="px-2 py-1 rounded text-xs font-medium {{ $action?->badgeColor() ?? 'bg-gray-100 text-gray-600' }}">
        {{ $action?->label() ?? ucfirst(str_replace('_', ' ', $log->action)) }}
    </span>
@endforeach
js
module.exports = {
  content: [
    './resources/**/*.blade.php',
    './vendor/williamug/audited/resources/views/**/*.blade.php', // ← add this
  ],
  // ...
}
bash
php artisan vendor:publish --tag=audited-views
bash
php artisan vendor:publish --tag=audited-vue
bash
php artisan queue:work --queue=audit
bash
php artisan make:migration upgrade_audit_logs_table