Skip to content

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.

php
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:

php
SendWelcomeEmail::sync()->execute($user);

The Two Traits

TraitWhen to use
ExecutableSync-only actions (no queue support)
QueueableExecutableActions 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:

php
// 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:

php
// 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:

php
class ConcatenateInput
{
    use Executable;

    public function __construct(private InputService $service) {}

    public function execute(string ...$input): string
    {
        return $this->service->concatenate(...$input);
    }
}