Adminix Documentation Help

Audit

AuditPanelModule renders activity events for a server-defined subject. Adminix does not ship persistence for audit rows; consuming applications provide a reader through AuditProviderInterface and can provide a writer through AuditRecorderInterface.

Audit panel

Add the module to an Adminix page:

use AlexKudrya\Adminix\AdminixPage; use AlexKudrya\Adminix\Modules\Audit\AuditPanelModule; use App\Adminix\Audit\DatabaseAuditProvider; use App\Models\User; return (new AdminixPage()) ->uri('users') ->name('users-show') ->addModule( AuditPanelModule::make('user_activity') ->title('Activity') ->subject(User::class, 'param:0') ->provider(DatabaseAuditProvider::class) ->limit(25) );

Result: /adminix/users/15 resolves param:0 to 15, calls the configured provider with User::class and subject id 15, and renders a timeline with actor, action, timestamp, field diff, and metadata. The subject id comes from Adminix page params, not from query strings or hidden browser input.

The same AuditPanelModule can also be attached to a DetailModule through auditPanel()/withAuditPanel(). In that case Adminix prepares the detail record first, then prepares the audit panel with the same page params and renders it below the detail card and relation snippets.

provider() accepts:

  • an AuditProviderInterface instance;

  • a class string resolved through the Laravel container;

  • a callable with signature fn (AuditSubjectDto $subject, AuditPanelModule $module): AuditResultDto|array.

Provider example:

use AlexKudrya\Adminix\Contracts\AuditProviderInterface; use AlexKudrya\Adminix\Dto\AuditEventDto; use AlexKudrya\Adminix\Dto\AuditResultDto; use AlexKudrya\Adminix\Dto\AuditSubjectDto; use AlexKudrya\Adminix\Modules\Audit\AuditPanelModule; use App\Models\AuditLog; final class DatabaseAuditProvider implements AuditProviderInterface { public function events(AuditSubjectDto $subject, AuditPanelModule $module): AuditResultDto { $rows = AuditLog::query() ->where('subject_type', $subject->type()) ->where('subject_id', $subject->id()) ->latest('occurred_at') ->limit($module->limit()) ->get(); return AuditResultDto::fromEvents( $rows->map(fn (AuditLog $row) => AuditEventDto::fromArray([ 'action' => $row->action, 'subject_type' => $row->subject_type, 'subject_id' => $row->subject_id, 'actor_type' => $row->actor_type, 'actor_id' => $row->actor_id, 'actor_label' => $row->actor_label, 'changes' => $row->changes ?? [], 'metadata' => $row->metadata ?? [], 'occurred_at' => $row->occurred_at, ]))->all(), $module->page(), $module->limit(), ); } }

If the provider throws, Adminix renders a controlled warning inside the panel instead of a 500 response.

Audit events

Adminix dispatches package events after successful package flows:

  • ResourceCreated;

  • ResourceUpdated;

  • ResourceUploadChanged;

  • BulkActionQueued;

  • BulkActionExecuted.

Each event exposes auditEvent(): AuditEventDto.

use AlexKudrya\Adminix\Events\ResourceUpdated; use Illuminate\Support\Facades\Event; Event::listen(ResourceUpdated::class, function (ResourceUpdated $event): void { $audit = $event->auditEvent(); // Persist $audit->action(), $audit->subjectType(), $audit->subjectId(), // $audit->actorLabel(), $audit->changes(), and $audit->metadata(). });

Result: listeners receive a normalized event after the resource update has succeeded. For updates, changes() contains only fields whose submitted writable value differs from the current server-loaded row. For queued bulk actions, bulk.queued metadata contains the server-resolved action name, selected ids after list scope filtering, primary key, queue connection/name, success flag, and queued message. When the queued handler later runs, Adminix records the normal bulk.executed event with the handler result. For synchronous bulk actions, bulk.executed metadata contains the server-resolved action name, selected ids after list scope filtering, primary key, success flag, and handler message. For uploads, metadata() contains stored and replaced file descriptors.

Audit recorder

By default Adminix binds a no-op AuditRecorderInterface, so enabling audit events does not require a database table or extra package dependency. To persist every emitted audit event, bind your own recorder in the consuming Laravel app:

use AlexKudrya\Adminix\Contracts\AuditRecorderInterface; use AlexKudrya\Adminix\Dto\AuditEventDto; use App\Models\AuditLog; final class DatabaseAuditRecorder implements AuditRecorderInterface { public function record(AuditEventDto $event): void { AuditLog::query()->create($event->toArray()); } }
use AlexKudrya\Adminix\Contracts\AuditRecorderInterface; use App\Adminix\Audit\DatabaseAuditRecorder; public function register(): void { $this->app->singleton(AuditRecorderInterface::class, DatabaseAuditRecorder::class); }

Result: resource create/update/upload and bulk action flows call your recorder before dispatching the package event. Recorder exceptions are swallowed by Adminix so audit persistence failures do not break the admin write flow; log failures inside your recorder if your application needs operational visibility.

Safety notes

  • Keep tenant/user ownership in server-side module config and provider queries.

  • Do not read subject ids, writable fields, selected ids, or datasource names from browser metadata when persisting audit records.

  • AuditPanelModule::subject(..., 'param:n') resolves only from Adminix page params, including when the panel is attached to DetailModule.

  • Bulk audit selected ids are already filtered through the same list datasource, criteria, filters, search, sorting, lens, and relation scope used by the rendered list.

  • Upload audit metadata contains file descriptors; do not expose private storage URLs from a provider unless they are safe for the current admin user.

Last modified: 22 June 2026