Skip to content

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.

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

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 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);
    }
}