Skip to content

Commit 19e7ca0

Browse files
authored
Merge branch '1.x' into l10n_1.x
2 parents 5584267 + 00ad551 commit 19e7ca0

83 files changed

Lines changed: 54306 additions & 7135 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/mcp.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"mcpServers": {
3+
"laravel-boost": {
4+
"command": "vendor/bin/sail",
5+
"args": [
6+
"artisan",
7+
"boost:mcp"
8+
]
9+
}
10+
}
11+
}
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

Comments
 (0)