Getting Started
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. Single responsibility, always.
execute()is the only required method. Its signature is yours to define.- 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 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);
}
}