Getting Started
Single-task action classes for Laravel, built around a testing API you'll actually want to use. Fake executions, spy on calls, assert arguments, queue configuration and behaviour. All in a few readable lines that make your test files the cleanest code in the project.
The Core Idea
Executables are single-task action classes. No base class, no interface required. Just a plain PHP class with a trait and an execute() method.
use Havn\Executable\Executable;
class SendWelcomeEmail
{
use Executable;
public function execute(User $user): void
{
Mail::to($user->email)->send(new WelcomeMail($user));
}
}- One class = one task.
execute()is the only required method.- The trait provides static entry points. The class itself stays clean.
Executables support Laravel's full queue feature set. Properties like $tries and $timeout, interfaces like ShouldBeEncrypted, middleware, chains, batches. It all works the same as with native Laravel jobs.
Calling it:
SendWelcomeEmail::sync()->execute($user);The Two Traits
| Trait | When to use |
|---|---|
Executable | Sync-only actions (no queue support) |
QueueableExecutable | Actions that can run sync or on a queue |
QueueableExecutable includes everything Executable does, plus queue capabilities.
If the action might ever need to be queued, use QueueableExecutable. It still supports ::sync(), but also unlocks ::onQueue() and ::prepare().
Execution Modes
Four execution modes:
// Sync: runs immediately, returns the result
$result = ProcessOrder::sync()->execute($order);
// Queue: dispatches to the queue
ProcessOrder::onQueue()->execute($order);
// Prepare: returns a job object without dispatching (for chains/batches)
$job = ProcessOrder::prepare()->execute($order);
// Test: runs the real code in a testable context
ProcessOrder::test()->execute($order);These four modes are the entire API surface for calling executables. Everything else is configuration or testing.
Always use the static entry points. They route through the execution pipeline, which handles concurrency limiting, transactions, conditional execution, and after-response dispatch. Calling ->execute() directly on an instance bypasses all of that:
// Good: goes through the pipeline
ProcessOrder::sync()->execute($order);
// Also fine: resolve from the container, then use ->sync()
$action = app(ProcessOrder::class);
$action->sync()->execute($order);
// Bad: bypasses the pipeline
$action = app(ProcessOrder::class);
$action->execute($order); Constructor Injection
Executables are resolved through the Laravel container. Constructor injection works exactly as expected:
class ConcatenateInput
{
use Executable;
public function __construct(private InputService $service) {}
public function execute(string ...$input): string
{
return $this->service->concatenate(...$input);
}
}