|
| 1 | +--- |
| 2 | +name: laravel-actions |
| 3 | +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. |
| 4 | +--- |
| 5 | + |
| 6 | +# Laravel Actions or `lorisleiva/laravel-actions` |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns. |
| 11 | + |
| 12 | +## Quick Workflow |
| 13 | + |
| 14 | +1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`. |
| 15 | +2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`. |
| 16 | +3. Implement `handle(...)` with the core business logic first. |
| 17 | +4. Add adapter methods only when needed for the requested entrypoint: |
| 18 | + - `asController` (+ route/invokable controller usage) |
| 19 | + - `asJob` (+ dispatch) |
| 20 | + - `asListener` (+ event listener wiring) |
| 21 | + - `asCommand` (+ command signature/description) |
| 22 | +5. Add or update tests for the chosen entrypoint. |
| 23 | +6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`). |
| 24 | + |
| 25 | +## Base Action Pattern |
| 26 | + |
| 27 | +Use this minimal skeleton and expand only what is needed. |
| 28 | + |
| 29 | +```php |
| 30 | +<?php |
| 31 | + |
| 32 | +namespace App\Actions; |
| 33 | + |
| 34 | +use Lorisleiva\Actions\Concerns\AsAction; |
| 35 | + |
| 36 | +class PublishArticle |
| 37 | +{ |
| 38 | + use AsAction; |
| 39 | + |
| 40 | + public function handle(int $articleId): bool |
| 41 | + { |
| 42 | + return true; |
| 43 | + } |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +## Project Conventions |
| 48 | + |
| 49 | +- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used. |
| 50 | +- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`). |
| 51 | +- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`). |
| 52 | +- Prefer explicit parameter and return types in all action methods. |
| 53 | +- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments. |
| 54 | + |
| 55 | +### When to Use an Action |
| 56 | + |
| 57 | +- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking. |
| 58 | +- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action. |
| 59 | + |
| 60 | +## Entrypoint Patterns |
| 61 | + |
| 62 | +### Run as Object |
| 63 | + |
| 64 | +- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`. |
| 65 | +- Use make and call handle: `PublishArticle::make()->handle($id)`. |
| 66 | +- Call with dependency injection: `app(PublishArticle::class)->handle($id)`. |
| 67 | + |
| 68 | +### Run as Controller |
| 69 | + |
| 70 | +- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`. |
| 71 | +- Add `asController(...)` for HTTP-specific adaptation and return a response. |
| 72 | +- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP. |
| 73 | + |
| 74 | +### Run as Job |
| 75 | + |
| 76 | +- Dispatch with `PublishArticle::dispatch($id)`. |
| 77 | +- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`. |
| 78 | +- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control. |
| 79 | + |
| 80 | +#### Project Pattern: Job Action with Extra Methods |
| 81 | + |
| 82 | +```php |
| 83 | +<?php |
| 84 | + |
| 85 | +namespace App\Actions\Demo; |
| 86 | + |
| 87 | +use App\Models\Demo; |
| 88 | +use DateTime; |
| 89 | +use Lorisleiva\Actions\Concerns\AsAction; |
| 90 | +use Lorisleiva\Actions\Decorators\JobDecorator; |
| 91 | + |
| 92 | +class GetDemoData |
| 93 | +{ |
| 94 | + use AsAction; |
| 95 | + |
| 96 | + public int $jobTries = 3; |
| 97 | + |
| 98 | + public int $jobMaxExceptions = 3; |
| 99 | + |
| 100 | + public function getJobRetryUntil(): DateTime |
| 101 | + { |
| 102 | + return now()->addMinutes(30); |
| 103 | + } |
| 104 | + |
| 105 | + public function getJobBackoff(): array |
| 106 | + { |
| 107 | + return [60, 120]; |
| 108 | + } |
| 109 | + |
| 110 | + public function getJobUniqueId(Demo $demo): string |
| 111 | + { |
| 112 | + return $demo->id; |
| 113 | + } |
| 114 | + |
| 115 | + public function handle(Demo $demo): void |
| 116 | + { |
| 117 | + // Core business logic. |
| 118 | + } |
| 119 | + |
| 120 | + public function asJob(JobDecorator $job, Demo $demo): void |
| 121 | + { |
| 122 | + // Queue-specific orchestration and retry behavior. |
| 123 | + $this->handle($demo); |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +Use these members only when needed: |
| 129 | + |
| 130 | +- `$jobTries`: max attempts for the queued execution. |
| 131 | +- `$jobMaxExceptions`: max unhandled exceptions before failing. |
| 132 | +- `getJobRetryUntil()`: absolute retry deadline. |
| 133 | +- `getJobBackoff()`: retry delay strategy per attempt. |
| 134 | +- `getJobUniqueId(...)`: deduplication key for unique jobs. |
| 135 | +- `asJob(JobDecorator $job, ...)`: access attempt metadata and queue-only branching. |
| 136 | + |
| 137 | +### Run as Listener |
| 138 | + |
| 139 | +- Register the action class as listener in `EventServiceProvider`. |
| 140 | +- Use `asListener(EventName $event)` and delegate to `handle(...)`. |
| 141 | + |
| 142 | +### Run as Command |
| 143 | + |
| 144 | +- Define `$commandSignature` and `$commandDescription` properties. |
| 145 | +- Implement `asCommand(Command $command)` and keep console IO in this method only. |
| 146 | +- Import `Command` with `use Illuminate\Console\Command;`. |
| 147 | + |
| 148 | +## Testing Guidance |
| 149 | + |
| 150 | +Use a two-layer strategy: |
| 151 | + |
| 152 | +1. `handle(...)` tests for business correctness. |
| 153 | +2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration. |
| 154 | + |
| 155 | +### Deep Dive: `AsFake` methods (2.x) |
| 156 | + |
| 157 | +Reference: https://www.laravelactions.com/2.x/as-fake.html |
| 158 | + |
| 159 | +Use these methods intentionally based on what you want to prove. |
| 160 | + |
| 161 | +#### `mock()` |
| 162 | + |
| 163 | +- Replaces the action with a full mock. |
| 164 | +- Best when you need strict expectations and argument assertions. |
| 165 | + |
| 166 | +```php |
| 167 | +PublishArticle::mock() |
| 168 | + ->shouldReceive('handle') |
| 169 | + ->once() |
| 170 | + ->with(42) |
| 171 | + ->andReturnTrue(); |
| 172 | +``` |
| 173 | + |
| 174 | +#### `partialMock()` |
| 175 | + |
| 176 | +- Replaces the action with a partial mock. |
| 177 | +- Best when you want to keep most real behavior but stub one expensive/internal method. |
| 178 | + |
| 179 | +```php |
| 180 | +PublishArticle::partialMock() |
| 181 | + ->shouldReceive('fetchRemoteData') |
| 182 | + ->once() |
| 183 | + ->andReturn(['ok' => true]); |
| 184 | +``` |
| 185 | + |
| 186 | +#### `spy()` |
| 187 | + |
| 188 | +- Replaces the action with a spy. |
| 189 | +- Best for post-execution verification ("was called with X") without predefining all expectations. |
| 190 | + |
| 191 | +```php |
| 192 | +$spy = PublishArticle::spy()->allows('handle')->andReturnTrue(); |
| 193 | + |
| 194 | +// execute code that triggers the action... |
| 195 | + |
| 196 | +$spy->shouldHaveReceived('handle')->with(42); |
| 197 | +``` |
| 198 | + |
| 199 | +#### `shouldRun()` |
| 200 | + |
| 201 | +- Shortcut for `mock()->shouldReceive('handle')`. |
| 202 | +- Best for compact orchestration assertions. |
| 203 | + |
| 204 | +```php |
| 205 | +PublishArticle::shouldRun()->once()->with(42)->andReturnTrue(); |
| 206 | +``` |
| 207 | + |
| 208 | +#### `shouldNotRun()` |
| 209 | + |
| 210 | +- Shortcut for `mock()->shouldNotReceive('handle')`. |
| 211 | +- Best for guard-clause tests and branch coverage. |
| 212 | + |
| 213 | +```php |
| 214 | +PublishArticle::shouldNotRun(); |
| 215 | +``` |
| 216 | + |
| 217 | +#### `allowToRun()` |
| 218 | + |
| 219 | +- Shortcut for spy + allowing `handle`. |
| 220 | +- Best when you want execution to proceed but still assert interaction. |
| 221 | + |
| 222 | +```php |
| 223 | +$spy = PublishArticle::allowToRun()->andReturnTrue(); |
| 224 | +// ... |
| 225 | +$spy->shouldHaveReceived('handle')->once(); |
| 226 | +``` |
| 227 | + |
| 228 | +#### `isFake()` and `clearFake()` |
| 229 | + |
| 230 | +- `isFake()` checks whether the class is currently swapped. |
| 231 | +- `clearFake()` resets the fake and prevents cross-test leakage. |
| 232 | + |
| 233 | +```php |
| 234 | +expect(PublishArticle::isFake())->toBeFalse(); |
| 235 | +PublishArticle::mock(); |
| 236 | +expect(PublishArticle::isFake())->toBeTrue(); |
| 237 | +PublishArticle::clearFake(); |
| 238 | +expect(PublishArticle::isFake())->toBeFalse(); |
| 239 | +``` |
| 240 | + |
| 241 | +### Recommended test matrix for Actions |
| 242 | + |
| 243 | +- Business rule test: call `handle(...)` directly with real dependencies/factories. |
| 244 | +- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`. |
| 245 | +- Job wiring test: dispatch action as job, assert expected downstream action calls. |
| 246 | +- Event listener test: dispatch event, assert action interaction via fake/spy. |
| 247 | +- Console test: run artisan command, assert action invocation and output. |
| 248 | + |
| 249 | +### Practical defaults |
| 250 | + |
| 251 | +- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests. |
| 252 | +- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification. |
| 253 | +- Prefer `mock()` when interaction contracts are strict and should fail fast. |
| 254 | +- Use `clearFake()` in cleanup when a fake might leak into another test. |
| 255 | +- Keep side effects isolated: fake only the action under test boundary, not everything. |
| 256 | + |
| 257 | +### Pest style examples |
| 258 | + |
| 259 | +```php |
| 260 | +it('dispatches the downstream action', function () { |
| 261 | + SendInvoiceEmail::shouldRun()->once()->withArgs(fn (int $invoiceId) => $invoiceId > 0); |
| 262 | + |
| 263 | + FinalizeInvoice::run(123); |
| 264 | +}); |
| 265 | + |
| 266 | +it('does not dispatch when invoice is already sent', function () { |
| 267 | + SendInvoiceEmail::shouldNotRun(); |
| 268 | + |
| 269 | + FinalizeInvoice::run(123, alreadySent: true); |
| 270 | +}); |
| 271 | +``` |
| 272 | + |
| 273 | +Run the minimum relevant suite first, e.g. `php artisan test --compact --filter=PublishArticle` or by specific test file. |
| 274 | + |
| 275 | +## Troubleshooting Checklist |
| 276 | + |
| 277 | +- Ensure the class uses `AsAction` and namespace matches autoload. |
| 278 | +- Check route registration when used as controller. |
| 279 | +- Check queue config when using `dispatch`. |
| 280 | +- Verify event-to-listener mapping in `EventServiceProvider`. |
| 281 | +- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`. |
| 282 | + |
| 283 | +## Common Pitfalls |
| 284 | + |
| 285 | +- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`. |
| 286 | +- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`. |
| 287 | +- Assuming listener wiring works without explicit registration where required. |
| 288 | +- Testing only entrypoints and skipping direct `handle(...)` behavior tests. |
| 289 | +- Overusing Actions for one-off, single-context logic with no reuse pressure. |
| 290 | + |
| 291 | +## Topic References |
| 292 | + |
| 293 | +Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules. |
| 294 | + |
| 295 | +- Object entrypoint: `references/object.md` |
| 296 | +- Controller entrypoint: `references/controller.md` |
| 297 | +- Job entrypoint: `references/job.md` |
| 298 | +- Listener entrypoint: `references/listener.md` |
| 299 | +- Command entrypoint: `references/command.md` |
| 300 | +- With attributes: `references/with-attributes.md` |
| 301 | +- Testing and fakes: `references/testing-fakes.md` |
| 302 | +- Troubleshooting: `references/troubleshooting.md` |
0 commit comments