Search by

directorytree / runnable

stevebauman

Run and fake focused PHP classes using Laravel's container.

Package info

github.com/DirectoryTree/Runnable

pkg:composer/directorytree/runnable

Fund package maintenance!

stevebauman

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-24 06:28 UTC

This package is auto-updated.

Last update: 2026-09-27 20:02:49 UTC


README

Run and fake focused PHP classes using Laravel's container.

Tests Total Downloads Latest Version License

Installation · Usage · Testing

Runnable gives your action and query classes a familiar way to run, with results you can easily fake in tests.

$response = ProcessPayment::run($order);

Test an order payment without contacting the payment provider:

use function Pest\Laravel\post;
use function Pest\Laravel\actingAs;

it('can pay for an order', function () {
    $user = User::factory()->create();

    $order = Order::factory()->for($user)->create([
        'total' => 5000,
        'status' => 'pending',
    ]);

    actingAs($user);

    $action = ProcessPayment::fake(
        new PaymentResponse(success: true),
    );

    post(route('orders.pay', $order))
        ->assertRedirect(route('orders.show', $order));

    $action->shouldHaveReceived('handle')->once();
});

Requirements

  • PHP 8.2 or higher (PHP 8.3 or higher for Laravel 13)
  • Laravel 12 or 13

Installation

composer require directorytree/runnable

The service provider is automatically registered. There is no configuration to publish.

Usage

Add the Runnable trait to a class with a public handle() method:

namespace App\Actions;

use App\Models\Order;
use Stripe\StripeClient;
use App\Payments\PaymentResponse;
use DirectoryTree\Runnable\Runnable;

class ProcessPayment
{
    use Runnable;

    public function __construct(
        protected StripeClient $stripe
    ) {}

    public function handle(Order $order): PaymentResponse
    {
        $payment = $this->stripe->paymentIntents->create([
            // ...
        ]);

        return new PaymentResponse(success: $payment->status === 'succeeded');
    }
}

Running Classes

Using the trait:

use App\Actions\ProcessPayment;

$response = ProcessPayment::run($order);

Using the facade:

use App\Actions\ProcessPayment;
use DirectoryTree\Runnable\Facades\Run;

$response = Run::execute(ProcessPayment::class, $order);

Using the helper:

use App\Actions\ProcessPayment;

use function DirectoryTree\Runnable\run;

$response = run(ProcessPayment::class, $order);

All three run handle() synchronously and return its result. Arguments, including named arguments, are forwarded to handle(), and exceptions propagate to the caller. For the facade and helper, pass the class as the first positional argument.

The facade and helper work without the trait. You can also inject your class and call handle() directly.

Running Instances

Using the facade:

use App\Actions\ProcessPayment;
use DirectoryTree\Runnable\Facades\Run;

$response = Run::execute(new ProcessPayment($stripe), $order);

Using the helper:

use App\Actions\ProcessPayment;

use function DirectoryTree\Runnable\run;

$response = run(new ProcessPayment($stripe), $order);

Runnable executes the supplied instance unless you have registered a fake for its class. Ordinary container bindings do not replace it.

Testing

Call fake() before exercising your application code to replace a runnable's result and verify its calls:

use App\Actions\ProcessPayment;

use function DirectoryTree\Runnable\run;

$fake = ProcessPayment::fake($response);

$result = run(new ProcessPayment($stripe), $order);

expect($result)->toBe($response);

$fake->shouldHaveReceived('handle')->with($order)->once();

The same fake works with all three execution styles and subsequent container resolutions, including dependency injection. Use it within Laravel application tests so the container and Mockery are reset between tests.

Fakes intercept handle(), so an inline instance's constructor still runs. Calling handle() directly on a real instance bypasses Runnable.

The returned fake supports Mockery assertions, including checking that it was never called:

$fake->shouldNotHaveReceived('handle');

You can also register a fake through the facade, including for classes without the trait:

use DirectoryTree\Runnable\Facades\Run;

$fake = Run::fake(ProcessPayment::class, $response);

Pass null explicitly to return null. Omitting the result uses Mockery's default for the method's return type. Supplied results must satisfy that type.

Faking Callbacks

Provide a closure when the result depends on the arguments:

ProcessPayment::fake(fn (Order $order) => new PaymentResponse(
    success: $order->total <= 5000,
));