Build, refactor, and troubleshoot Laravel Actions using lorisleiva/laravel-actions. Use when implementing reusable action classes (object/controller/job/listener/command), converting service classes/controllers/jobs into actions, orchestrating workflows via faked actions, or debugging action entrypoints and wiring.
lorisleiva/laravel-actionsUse this skill to implement or update actions based on lorisleiva/laravel-actions with consistent structure and predictable testing patterns.
composer show lorisleiva/laravel-actions.Lorisleiva\Actions\Concerns\AsAction.handle(...) with the core business logic first.asController (+ route/invokable controller usage)asJob (+ dispatch)asListener (+ event listener wiring)asCommand (+ command signature/description)MyAction::fake()) and assertions (MyAction::assertDispatched()).Use this minimal skeleton and expand only what is needed.
<?php
namespace App\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
return true;
}
}
App\Actions unless an existing domain sub-namespace is already used.VerbNoun naming (e.g. PublishArticle, SyncVehicleTaxStatus).handle(...); keep transport and framework concerns in adapter methods (asController, asJob, asListener, asCommand).PublishArticle::run($id).PublishArticle::make()->handle($id).app(PublishArticle::class)->handle($id).Route::post('/articles/{id}/publish', PublishArticle::class).asController(...) for HTTP-specific adaptation and return a response.rules() or custom validator hooks) when input comes from HTTP.PublishArticle::dispatch($id).asJob(...) only for queue-specific behavior; keep domain logic in handle(...).<?php
namespace App\Actions\Demo;
use App\Models\Demo;
use DateTime;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Decorators\JobDecorator;
class GetDemoData
{
use AsAction;
public int $jobTries = 3;
public int $jobMaxExceptions = 3;
public function getJobRetryUntil(): DateTime
{
return now()->addMinutes(30);
}
public function getJobBackoff(): array
{
return [60, 120];
}
public function getJobUniqueId(Demo $demo): string
{
return $demo->id;
}
public function handle(Demo $demo): void
{
// Core business logic.
}
public function asJob(JobDecorator $job, Demo $demo): void
{
// Queue-specific orchestration and retry behavior.
$this->handle($demo);
}
}
Use these members only when needed:
$jobTries: max attempts for the queued execution.$jobMaxExceptions: max unhandled exceptions before failing.getJobRetryUntil(): absolute retry deadline.getJobBackoff(): retry delay strategy per attempt.getJobUniqueId(...): deduplication key for unique jobs.asJob(JobDecorator $job, ...): access attempt metadata and queue-only branching.EventServiceProvider.asListener(EventName $event) and delegate to handle(...).$commandSignature and $commandDescription properties.asCommand(Command $command) and keep console IO in this method only.Command with use Illuminate\Console\Command;.Use a two-layer strategy:
handle(...) tests for business correctness.asController, asJob, asListener, asCommand) for wiring/orchestration.AsFake methods (2.x)Use these methods intentionally based on what you want to prove.
mock()PublishArticle::mock()
->shouldReceive('handle')
->once()
->with(42)
->andReturnTrue();
partialMock()PublishArticle::partialMock()
->shouldReceive('fetchRemoteData')
->once()
->andReturn(['ok' => true]);
spy()$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
// execute code that triggers the action...
$spy->shouldHaveReceived('handle')->with(42);
shouldRun()mock()->shouldReceive('handle').PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
shouldNotRun()mock()->shouldNotReceive('handle').PublishArticle::shouldNotRun();
allowToRun()handle.$spy = PublishArticle::allowToRun()->andReturnTrue();
// ...
$spy->shouldHaveReceived('handle')->once();
isFake() and clearFake()isFake() checks whether the class is currently swapped.clearFake() resets the fake and prevents cross-test leakage.expect(PublishArticle::isFake())->toBeFalse();
PublishArticle::mock();
expect(PublishArticle::isFake())->toBeTrue();
PublishArticle::clearFake();
expect(PublishArticle::isFake())->toBeFalse();
handle(...) directly with real dependencies/factories.shouldRun or shouldNotRun.shouldRun() and shouldNotRun() for readability in branch tests.spy()/allowToRun() when behavior is mostly real and you only need call verification.mock() when interaction contracts are strict and should fail fast.clearFake() in cleanup when a fake might leak into another test.it('dispatches the downstream action', function () {
SendInvoiceEmail::shouldRun()->once()->withArgs(fn (int $invoiceId) => $invoiceId > 0);
FinalizeInvoice::run(123);
});
it('does not dispatch when invoice is already sent', function () {
SendInvoiceEmail::shouldNotRun();
FinalizeInvoice::run(123, alreadySent: true);
});
Run the minimum relevant suite first, e.g. php artisan test --compact --filter=PublishArticle or by specific test file.
AsAction and namespace matches autoload.dispatch.EventServiceProvider.asController, asCommand, etc.), not in handle(...).handle(...) instead of asController(...).as* methods rather than delegating to handle(...).handle(...) behavior tests.Use these references for deep dives by entrypoint/topic. Keep SKILL.md focused on workflow and decision rules.
references/object.mdreferences/controller.mdreferences/job.mdreferences/listener.mdreferences/command.mdreferences/with-attributes.mdreferences/testing-fakes.mdreferences/troubleshooting.md37:["$","$L40",null,{"content":"$41","frontMatter":{"name":"laravel-actions","description":"Build, refactor, and troubleshoot Laravel Actions using lorisleiva/laravel-actions. Use when implementing reusable action classes (object/controller/job/listener/command), converting service classes/controllers/jobs into actions, orchestrating workflows via faked actions, or debugging action entrypoints and wiring."}}]