directorytree / runnable
Run and fake focused PHP classes using Laravel's container.
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/container: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
Suggests
- mockery/mockery: Required to fake runnable classes (^1.6).
Provides
None
Conflicts
None
Replaces
None
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.
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, ));