AI/Blogfor Laravel

Command bus, idempotency and audit trail

Updated

Every change can go through one bus, so a dashboard, an Artisan command, a job and an AI agent all get the same rules: the actor must hold the command's ability, every request carries an idempotency key (a retry replays the stored result instead of running twice), and each outcome is written to an audit trail.

Request key, params Key seen? Ability Gate check Validate rules() Handler handle() Replay stored result replayed = true, nothing runs yes no Audit trail: every outcome recorded refused invalid result
A retry with the same key gets the stored result back. A new request has to pass the ability check and validation before the handler runs, and every outcome is audited.
use ByteMinus\AiBlog\Bus\Actor;
use ByteMinus\AiBlog\Bus\CommandBus;
use ByteMinus\AiBlog\Bus\CommandContext;
use ByteMinus\AiBlog\Bus\CommandHandler;
use ByteMinus\AiBlog\Bus\CommandOutcome;
use ByteMinus\AiBlog\Bus\CommandRequest;

final class RenamePost extends CommandHandler
{
    public function ability(): ?string { return 'posts.write'; }

    public function rules(): array { return ['title' => ['required', 'string', 'max:120']]; }

    public function handle(CommandContext $context): CommandOutcome
    {
        // ...call your service here...
        return CommandOutcome::completed(['title' => $context->params['title']], 'post', '42');
    }
}
// config/ai-blog.php
'commands' => ['handlers' => ['post.rename' => RenamePost::class]],

// AppServiceProvider: nobody manages the blog until you say who may
Gate::define('manageAiBlog', fn ($user) => $user->is_admin);

// anywhere
$receipt = app(CommandBus::class)->dispatch(
    Actor::user($request->user()),
    new CommandRequest('post.rename', idempotencyKey: 'rename-42-v1', params: ['title' => 'New title']),
);

$receipt->result();    // what the caller may see
$receipt->replayed;    // true when this was a retry

Pass dryRun: true to preview a command without changing anything. Publish and run the migrations to create the commands and audit_events tables.

Edit on GitHubView as Markdown