Merge remote-tracking branch 'origin/next' into feat/refresh-repos

This commit is contained in:
Andras Bacsai 2026-03-26 19:00:08 +01:00
commit f2e4879742
518 changed files with 39180 additions and 4806 deletions

View file

@ -0,0 +1,85 @@
---
name: configuring-horizon
description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching."
license: MIT
metadata:
author: laravel
---
# Horizon Configuration
## Documentation
Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment.
For deeper guidance on specific topics, read the relevant reference file before implementing:
- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling
- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config
- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs
- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config
## Basic Usage
### Installation
```bash
php artisan horizon:install
```
### Supervisor Configuration
Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block:
<!-- Supervisor Config -->
```php
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
'environments' => [
'production' => [
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
],
'local' => [
'supervisor-1' => ['maxProcesses' => 2],
],
],
```
### Dashboard Authorization
Restrict access in `App\Providers\HorizonServiceProvider`:
<!-- Dashboard Gate -->
```php
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
return $user->is_admin;
});
}
```
## Verification
1. Run `php artisan horizon` and visit `/horizon`
2. Confirm dashboard access is restricted as expected
3. Check that metrics populate after scheduling `horizon:snapshot`
## Common Pitfalls
- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported.
- Redis Cluster is not supported. Horizon requires a standalone Redis connection.
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.

View file

@ -0,0 +1,21 @@
# Metrics & Snapshots
## Where to Find It
Search with `search-docs`:
- `"horizon metrics snapshot"` for the snapshot command and scheduling
- `"horizon trim snapshots"` for retention configuration
## What to Watch For
### Metrics dashboard stays blank until `horizon:snapshot` is scheduled
Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler.
### Register the snapshot in the scheduler rather than running it manually
A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+.
### `metrics.trim_snapshots` is a snapshot count, not a time duration
The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage.

View file

@ -0,0 +1,21 @@
# Notifications & Alerts
## Where to Find It
Search with `search-docs`:
- `"horizon notifications"` for Horizon's built-in notification routing helpers
- `"horizon long wait detected"` for LongWaitDetected event details
## What to Watch For
### `waits` in `config/horizon.php` controls the LongWaitDetected threshold
The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration.
### Use Horizon's built-in notification routing in `HorizonServiceProvider`
Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration.
### Failed job alerts are separate from Horizon's documented notification routing
Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API.

View file

@ -0,0 +1,27 @@
# Supervisor & Balancing Configuration
## Where to Find It
Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions:
- `"horizon supervisor configuration"` for the full options list
- `"horizon balancing strategies"` for auto, simple, and false modes
- `"horizon autoscaling workers"` for autoScalingStrategy details
- `"horizon environment configuration"` for the defaults and environments merge
## What to Watch For
### The `environments` array merges into `defaults` rather than replacing it
The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`.
### Use separate named supervisors to enforce queue priority
Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this.
### Use `balance: false` to keep a fixed number of workers on a dedicated queue
Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable.
### Set `balanceCooldown` to prevent rapid worker scaling under bursty load
When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle.

View file

@ -0,0 +1,21 @@
# Tags & Silencing
## Where to Find It
Search with `search-docs`:
- `"horizon tags"` for the tagging API and auto-tagging behaviour
- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options
## What to Watch For
### Eloquent model jobs are tagged automatically without any extra code
If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed.
### `silenced` hides jobs from the dashboard completed list but does not stop them from running
Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs.
### `silenced_tags` hides all jobs carrying a matching tag from the completed list
Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes.

View file

@ -1,6 +1,9 @@
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
name: fortify-development
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
license: MIT
metadata:
author: laravel
---
# Laravel Fortify Development
@ -39,7 +42,7 @@ ### Two-Factor Authentication Setup
```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
@ -75,14 +78,26 @@ ### SPA Authentication Setup
```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```
> Use `search-docs` for integration and SPA authentication patterns.
#### Two-Factor Authentication in SPA Mode
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
```json
{
"two_factor": true
}
```
## Best Practices
### Custom Authentication Logic

View file

@ -0,0 +1,302 @@
---
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.
---
# Laravel Actions or `lorisleiva/laravel-actions`
## Overview
Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns.
## Quick Workflow
1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`.
2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`.
3. Implement `handle(...)` with the core business logic first.
4. Add adapter methods only when needed for the requested entrypoint:
- `asController` (+ route/invokable controller usage)
- `asJob` (+ dispatch)
- `asListener` (+ event listener wiring)
- `asCommand` (+ command signature/description)
5. Add or update tests for the chosen entrypoint.
6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`).
## Base Action Pattern
Use this minimal skeleton and expand only what is needed.
```php
<?php
namespace App\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
return true;
}
}
```
## Project Conventions
- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used.
- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`).
- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`).
- Prefer explicit parameter and return types in all action methods.
- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments.
### When to Use an Action
- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking.
- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action.
## Entrypoint Patterns
### Run as Object
- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`.
- Use make and call handle: `PublishArticle::make()->handle($id)`.
- Call with dependency injection: `app(PublishArticle::class)->handle($id)`.
### Run as Controller
- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`.
- Add `asController(...)` for HTTP-specific adaptation and return a response.
- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP.
### Run as Job
- Dispatch with `PublishArticle::dispatch($id)`.
- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`.
- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control.
#### Project Pattern: Job Action with Extra Methods
```php
<?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.
### Run as Listener
- Register the action class as listener in `EventServiceProvider`.
- Use `asListener(EventName $event)` and delegate to `handle(...)`.
### Run as Command
- Define `$commandSignature` and `$commandDescription` properties.
- Implement `asCommand(Command $command)` and keep console IO in this method only.
- Import `Command` with `use Illuminate\Console\Command;`.
## Testing Guidance
Use a two-layer strategy:
1. `handle(...)` tests for business correctness.
2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration.
### Deep Dive: `AsFake` methods (2.x)
Reference: https://www.laravelactions.com/2.x/as-fake.html
Use these methods intentionally based on what you want to prove.
#### `mock()`
- Replaces the action with a full mock.
- Best when you need strict expectations and argument assertions.
```php
PublishArticle::mock()
->shouldReceive('handle')
->once()
->with(42)
->andReturnTrue();
```
#### `partialMock()`
- Replaces the action with a partial mock.
- Best when you want to keep most real behavior but stub one expensive/internal method.
```php
PublishArticle::partialMock()
->shouldReceive('fetchRemoteData')
->once()
->andReturn(['ok' => true]);
```
#### `spy()`
- Replaces the action with a spy.
- Best for post-execution verification ("was called with X") without predefining all expectations.
```php
$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
// execute code that triggers the action...
$spy->shouldHaveReceived('handle')->with(42);
```
#### `shouldRun()`
- Shortcut for `mock()->shouldReceive('handle')`.
- Best for compact orchestration assertions.
```php
PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
```
#### `shouldNotRun()`
- Shortcut for `mock()->shouldNotReceive('handle')`.
- Best for guard-clause tests and branch coverage.
```php
PublishArticle::shouldNotRun();
```
#### `allowToRun()`
- Shortcut for spy + allowing `handle`.
- Best when you want execution to proceed but still assert interaction.
```php
$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.
```php
expect(PublishArticle::isFake())->toBeFalse();
PublishArticle::mock();
expect(PublishArticle::isFake())->toBeTrue();
PublishArticle::clearFake();
expect(PublishArticle::isFake())->toBeFalse();
```
### Recommended test matrix for Actions
- Business rule test: call `handle(...)` directly with real dependencies/factories.
- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`.
- Job wiring test: dispatch action as job, assert expected downstream action calls.
- Event listener test: dispatch event, assert action interaction via fake/spy.
- Console test: run artisan command, assert action invocation and output.
### Practical defaults
- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests.
- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification.
- Prefer `mock()` when interaction contracts are strict and should fail fast.
- Use `clearFake()` in cleanup when a fake might leak into another test.
- Keep side effects isolated: fake only the action under test boundary, not everything.
### Pest style examples
```php
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.
## Troubleshooting Checklist
- Ensure the class uses `AsAction` and namespace matches autoload.
- Check route registration when used as controller.
- Check queue config when using `dispatch`.
- Verify event-to-listener mapping in `EventServiceProvider`.
- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`.
## Common Pitfalls
- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`.
- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`.
- Assuming listener wiring works without explicit registration where required.
- Testing only entrypoints and skipping direct `handle(...)` behavior tests.
- Overusing Actions for one-off, single-context logic with no reuse pressure.
## Topic References
Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules.
- Object entrypoint: `references/object.md`
- Controller entrypoint: `references/controller.md`
- Job entrypoint: `references/job.md`
- Listener entrypoint: `references/listener.md`
- Command entrypoint: `references/command.md`
- With attributes: `references/with-attributes.md`
- Testing and fakes: `references/testing-fakes.md`
- Troubleshooting: `references/troubleshooting.md`

View file

@ -0,0 +1,160 @@
# Command Entrypoint (`asCommand`)
## Scope
Use this reference when exposing actions as Artisan commands.
## Recap
- Documents command execution via `asCommand(...)` and fallback to `handle(...)`.
- Covers command metadata via methods/properties (signature, description, help, hidden).
- Includes registration example and focused artisan test pattern.
- Reinforces separation between console I/O and domain logic.
## Recommended pattern
- Define `$commandSignature` and `$commandDescription`.
- Implement `asCommand(Command $command)` for console I/O.
- Keep business logic in `handle(...)`.
## Methods used (`CommandDecorator`)
### `asCommand`
Called when executed as a command. If missing, it falls back to `handle(...)`.
```php
use Illuminate\Console\Command;
class UpdateUserRole
{
use AsAction;
public string $commandSignature = 'users:update-role {user_id} {role}';
public function handle(User $user, string $newRole): void
{
$user->update(['role' => $newRole]);
}
public function asCommand(Command $command): void
{
$this->handle(
User::findOrFail($command->argument('user_id')),
$command->argument('role')
);
$command->info('Done!');
}
}
```
### `getCommandSignature`
Defines the command signature. Required when registering an action as a command if no `$commandSignature` property is set.
```php
public function getCommandSignature(): string
{
return 'users:update-role {user_id} {role}';
}
```
### `$commandSignature`
Property alternative to `getCommandSignature`.
```php
public string $commandSignature = 'users:update-role {user_id} {role}';
```
### `getCommandDescription`
Provides command description.
```php
public function getCommandDescription(): string
{
return 'Updates the role of a given user.';
}
```
### `$commandDescription`
Property alternative to `getCommandDescription`.
```php
public string $commandDescription = 'Updates the role of a given user.';
```
### `getCommandHelp`
Provides additional help text shown with `--help`.
```php
public function getCommandHelp(): string
{
return 'My help message.';
}
```
### `$commandHelp`
Property alternative to `getCommandHelp`.
```php
public string $commandHelp = 'My help message.';
```
### `isCommandHidden`
Defines whether command should be hidden from artisan list. Default is `false`.
```php
public function isCommandHidden(): bool
{
return true;
}
```
### `$commandHidden`
Property alternative to `isCommandHidden`.
```php
public bool $commandHidden = true;
```
## Examples
### Register in console kernel
```php
// app/Console/Kernel.php
protected $commands = [
UpdateUserRole::class,
];
```
### Focused command test
```php
$this->artisan('users:update-role 1 admin')
->expectsOutput('Done!')
->assertSuccessful();
```
## Checklist
- `use Illuminate\Console\Command;` is imported.
- Signature/options/arguments are documented.
- Command test verifies invocation and output.
## Common pitfalls
- Mixing command I/O with domain logic in `handle(...)`.
- Missing/ambiguous command signature.
## References
- https://www.laravelactions.com/2.x/as-command.html

View file

@ -0,0 +1,339 @@
# Controller Entrypoint (`asController`)
## Scope
Use this reference when exposing an action through HTTP routes.
## Recap
- Documents controller lifecycle around `asController(...)` and response adapters.
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
- Summarizes validation/authorization hooks used by `ActionRequest`.
- Provides extension points for JSON/HTML responses and failure customization.
## Recommended pattern
- Route directly to action class when appropriate.
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
- Keep domain logic in `handle(...)`.
## Methods provided (`AsController` trait)
### `__invoke`
Required so Laravel can register the action class as an invokable controller.
```php
$action($someArguments);
// Equivalent to:
$action->handle($someArguments);
```
If the method does not exist, Laravel route registration fails for invokable controllers.
```php
// Illuminate\Routing\RouteAction
protected static function makeInvokable($action)
{
if (! method_exists($action, '__invoke')) {
throw new UnexpectedValueException("Invalid route action: [{$action}].");
}
return $action.'@__invoke';
}
```
If you need your own `__invoke`, alias the trait implementation:
```php
class MyAction
{
use AsAction {
__invoke as protected invokeFromLaravelActions;
}
public function __invoke()
{
// Custom behavior...
}
}
```
## Methods used (`ControllerDecorator` + `ActionRequest`)
### `asController`
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
```php
public function asController(User $user, Request $request): Response
{
$article = $this->handle(
$user,
$request->get('title'),
$request->get('body')
);
return redirect()->route('articles.show', [$article]);
}
```
### `jsonResponse`
Called after `asController` when request expects JSON.
```php
public function jsonResponse(Article $article, Request $request): ArticleResource
{
return new ArticleResource($article);
}
```
### `htmlResponse`
Called after `asController` when request expects HTML.
```php
public function htmlResponse(Article $article, Request $request): Response
{
return redirect()->route('articles.show', [$article]);
}
```
### `getControllerMiddleware`
Adds middleware directly on the action controller.
```php
public function getControllerMiddleware(): array
{
return ['auth', MyCustomMiddleware::class];
}
```
### `routes`
Defines routes directly in the action.
```php
public static function routes(Router $router)
{
$router->get('author/{author}/articles', static::class);
}
```
To enable this, register routes from actions in a service provider:
```php
use Lorisleiva\Actions\Facades\Actions;
Actions::registerRoutes();
Actions::registerRoutes('app/MyCustomActionsFolder');
Actions::registerRoutes([
'app/Authentication',
'app/Billing',
'app/TeamManagement',
]);
```
### `prepareForValidation`
Called before authorization and validation are resolved.
```php
public function prepareForValidation(ActionRequest $request): void
{
$request->merge(['some' => 'additional data']);
}
```
### `authorize`
Defines authorization logic.
```php
public function authorize(ActionRequest $request): bool
{
return $request->user()->role === 'author';
}
```
You can also return gate responses:
```php
use Illuminate\Auth\Access\Response;
public function authorize(ActionRequest $request): Response
{
if ($request->user()->role !== 'author') {
return Response::deny('You must be an author to create a new article.');
}
return Response::allow();
}
```
### `rules`
Defines validation rules.
```php
public function rules(): array
{
return [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
];
}
```
### `withValidator`
Adds custom validation logic with an after hook.
```php
use Illuminate\Validation\Validator;
public function withValidator(Validator $validator, ActionRequest $request): void
{
$validator->after(function (Validator $validator) use ($request) {
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
});
}
```
### `afterValidator`
Alternative to add post-validation checks.
```php
use Illuminate\Validation\Validator;
public function afterValidator(Validator $validator, ActionRequest $request): void
{
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
}
```
### `getValidator`
Provides a custom validator instead of default rules pipeline.
```php
use Illuminate\Validation\Factory;
use Illuminate\Validation\Validator;
public function getValidator(Factory $factory, ActionRequest $request): Validator
{
return $factory->make($request->only('title', 'body'), [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
]);
}
```
### `getValidationData`
Defines which data is validated (default: `$request->all()`).
```php
public function getValidationData(ActionRequest $request): array
{
return $request->all();
}
```
### `getValidationMessages`
Custom validation error messages.
```php
public function getValidationMessages(): array
{
return [
'title.required' => 'Looks like you forgot the title.',
'body.required' => 'Is that really all you have to say?',
];
}
```
### `getValidationAttributes`
Human-friendly names for request attributes.
```php
public function getValidationAttributes(): array
{
return [
'title' => 'headline',
'body' => 'content',
];
}
```
### `getValidationRedirect`
Custom redirect URL on validation failure.
```php
public function getValidationRedirect(UrlGenerator $url): string
{
return $url->to('/my-custom-redirect-url');
}
```
### `getValidationErrorBag`
Custom error bag name on validation failure (default: `default`).
```php
public function getValidationErrorBag(): string
{
return 'my_custom_error_bag';
}
```
### `getValidationFailure`
Override validation failure behavior.
```php
public function getValidationFailure(): void
{
throw new MyCustomValidationException();
}
```
### `getAuthorizationFailure`
Override authorization failure behavior.
```php
public function getAuthorizationFailure(): void
{
throw new MyCustomAuthorizationException();
}
```
## Checklist
- Route wiring points to the action class.
- `asController(...)` delegates to `handle(...)`.
- Validation/authorization methods are explicit where needed.
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
- HTTP tests cover both success and validation/authorization failure branches.
## Common pitfalls
- Putting response/redirect logic in `handle(...)`.
- Duplicating business rules in `asController(...)` instead of delegating.
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
## References
- https://www.laravelactions.com/2.x/as-controller.html

View file

@ -0,0 +1,425 @@
# Job Entrypoint (`dispatch`, `asJob`)
## Scope
Use this reference when running an action through queues.
## Recap
- Lists async/sync dispatch helpers and conditional dispatch variants.
- Covers job wrapping/chaining with `makeJob`, `makeUniqueJob`, and `withChain`.
- Documents queue assertion helpers for tests (`assertPushed*`).
- Summarizes `JobDecorator` hooks/properties for retries, uniqueness, timeout, and failure handling.
## Recommended pattern
- Dispatch with `Action::dispatch(...)` for async execution.
- Keep queue-specific orchestration in `asJob(...)`.
- Keep reusable business logic in `handle(...)`.
## Methods provided (`AsJob` trait)
### `dispatch`
Dispatches the action asynchronously.
```php
SendTeamReportEmail::dispatch($team);
```
### `dispatchIf`
Dispatches asynchronously only if condition is met.
```php
SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team);
```
### `dispatchUnless`
Dispatches asynchronously unless condition is met.
```php
SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);
```
### `dispatchSync`
Dispatches synchronously.
```php
SendTeamReportEmail::dispatchSync($team);
```
### `dispatchNow`
Alias of `dispatchSync`.
```php
SendTeamReportEmail::dispatchNow($team);
```
### `dispatchAfterResponse`
Dispatches synchronously after the HTTP response is sent.
```php
SendTeamReportEmail::dispatchAfterResponse($team);
```
### `makeJob`
Creates a `JobDecorator` wrapper. Useful with `dispatch(...)` helper or chains.
```php
dispatch(SendTeamReportEmail::makeJob($team));
```
### `makeUniqueJob`
Creates a `UniqueJobDecorator` wrapper. Usually automatic with `ShouldBeUnique`, but can be forced.
```php
dispatch(SendTeamReportEmail::makeUniqueJob($team));
```
### `withChain`
Attaches jobs to run after successful processing.
```php
$chain = [
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
];
CreateNewTeamReport::withChain($chain)->dispatch($team);
```
Equivalent using `Bus::chain(...)`:
```php
use Illuminate\Support\Facades\Bus;
Bus::chain([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
])->dispatch();
```
Chain assertion example:
```php
use Illuminate\Support\Facades\Bus;
Bus::fake();
Bus::assertChained([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
]);
```
### `assertPushed`
Asserts the action was queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushed();
SendTeamReportEmail::assertPushed(3);
SendTeamReportEmail::assertPushed($callback);
SendTeamReportEmail::assertPushed(3, $callback);
```
`$callback` receives:
- Action instance.
- Dispatched arguments.
- `JobDecorator` instance.
- Queue name.
### `assertNotPushed`
Asserts the action was not queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertNotPushed();
SendTeamReportEmail::assertNotPushed($callback);
```
### `assertPushedOn`
Asserts the action was queued on a specific queue.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushedOn('reports');
SendTeamReportEmail::assertPushedOn('reports', 3);
SendTeamReportEmail::assertPushedOn('reports', $callback);
SendTeamReportEmail::assertPushedOn('reports', 3, $callback);
```
## Methods used (`JobDecorator`)
### `asJob`
Called when dispatched as a job. Falls back to `handle(...)` if missing.
```php
class SendTeamReportEmail
{
use AsAction;
public function handle(Team $team, bool $fullReport = false): void
{
// Prepare report and send it to all $team->users.
}
public function asJob(Team $team): void
{
$this->handle($team, true);
}
}
```
### `getJobMiddleware`
Adds middleware to the queued action.
```php
public function getJobMiddleware(array $parameters): array
{
return [new RateLimited('reports')];
}
```
### `configureJob`
Configures `JobDecorator` options.
```php
use Lorisleiva\Actions\Decorators\JobDecorator;
public function configureJob(JobDecorator $job): void
{
$job->onConnection('my_connection')
->onQueue('my_queue')
->through(['my_middleware'])
->chain(['my_chain'])
->delay(60);
}
```
### `$jobConnection`
Defines queue connection.
```php
public string $jobConnection = 'my_connection';
```
### `$jobQueue`
Defines queue name.
```php
public string $jobQueue = 'my_queue';
```
### `$jobTries`
Defines max attempts.
```php
public int $jobTries = 10;
```
### `$jobMaxExceptions`
Defines max unhandled exceptions before failure.
```php
public int $jobMaxExceptions = 3;
```
### `$jobBackoff`
Defines retry delay seconds.
```php
public int $jobBackoff = 60;
```
### `getJobBackoff`
Defines retry delay (int or per-attempt array).
```php
public function getJobBackoff(): int
{
return 60;
}
public function getJobBackoff(): array
{
return [30, 60, 120];
}
```
### `$jobTimeout`
Defines timeout in seconds.
```php
public int $jobTimeout = 60 * 30;
```
### `$jobRetryUntil`
Defines timestamp retry deadline.
```php
public int $jobRetryUntil = 1610191764;
```
### `getJobRetryUntil`
Defines retry deadline as `DateTime`.
```php
public function getJobRetryUntil(): DateTime
{
return now()->addMinutes(30);
}
```
### `getJobDisplayName`
Customizes queued job display name.
```php
public function getJobDisplayName(): string
{
return 'Send team report email';
}
```
### `getJobTags`
Adds queue tags.
```php
public function getJobTags(Team $team): array
{
return ['report', 'team:'.$team->id];
}
```
### `getJobUniqueId`
Defines uniqueness key when using `ShouldBeUnique`.
```php
public function getJobUniqueId(Team $team): int
{
return $team->id;
}
```
### `$jobUniqueId`
Static uniqueness key alternative.
```php
public string $jobUniqueId = 'some_static_key';
```
### `getJobUniqueFor`
Defines uniqueness lock duration in seconds.
```php
public function getJobUniqueFor(Team $team): int
{
return $team->role === 'premium' ? 1800 : 3600;
}
```
### `$jobUniqueFor`
Property alternative for uniqueness lock duration.
```php
public int $jobUniqueFor = 3600;
```
### `getJobUniqueVia`
Defines cache driver used for uniqueness lock.
```php
public function getJobUniqueVia()
{
return Cache::driver('redis');
}
```
### `$jobDeleteWhenMissingModels`
Property alternative for missing model handling.
```php
public bool $jobDeleteWhenMissingModels = true;
```
### `getJobDeleteWhenMissingModels`
Defines whether jobs with missing models are deleted.
```php
public function getJobDeleteWhenMissingModels(): bool
{
return true;
}
```
### `jobFailed`
Handles job failure. Receives exception and dispatched parameters.
```php
public function jobFailed(?Throwable $e, ...$parameters): void
{
// Notify users, report errors, trigger compensations...
}
```
## Checklist
- Async/sync dispatch method matches use-case (`dispatch`, `dispatchSync`, `dispatchAfterResponse`).
- Queue config is explicit when needed (`$jobConnection`, `$jobQueue`, `configureJob`).
- Retry/backoff/timeout policies are intentional.
- `asJob(...)` delegates to `handle(...)` unless queue-specific branching is required.
- Queue tests use `Queue::fake()` and action assertions (`assertPushed*`).
## Common pitfalls
- Embedding domain logic only in `asJob(...)`.
- Forgetting uniqueness/timeout/retry controls on heavy jobs.
- Missing queue-specific assertions in tests.
## References
- https://www.laravelactions.com/2.x/as-job.html

View file

@ -0,0 +1,81 @@
# Listener Entrypoint (`asListener`)
## Scope
Use this reference when wiring actions to domain/application events.
## Recap
- Shows how listener execution maps event payloads into `handle(...)` arguments.
- Describes `asListener(...)` fallback behavior and adaptation role.
- Includes event registration example for provider wiring.
- Emphasizes test focus on dispatch and action interaction.
## Recommended pattern
- Register action listener in `EventServiceProvider` (or project equivalent).
- Use `asListener(Event $event)` for event adaptation.
- Delegate core logic to `handle(...)`.
## Methods used (`ListenerDecorator`)
### `asListener`
Called when executed as an event listener. If missing, it falls back to `handle(...)`.
```php
class SendOfferToNearbyDrivers
{
use AsAction;
public function handle(Address $source, Address $destination): void
{
// ...
}
public function asListener(TaxiRequested $event): void
{
$this->handle($event->source, $event->destination);
}
}
```
## Examples
### Event registration
```php
// app/Providers/EventServiceProvider.php
protected $listen = [
TaxiRequested::class => [
SendOfferToNearbyDrivers::class,
],
];
```
### Focused listener test
```php
use Illuminate\Support\Facades\Event;
Event::fake();
TaxiRequested::dispatch($source, $destination);
Event::assertDispatched(TaxiRequested::class);
```
## Checklist
- Event-to-listener mapping is registered.
- Listener method signature matches event contract.
- Listener tests verify dispatch and action interaction.
## Common pitfalls
- Assuming automatic listener registration when explicit mapping is required.
- Re-implementing business logic in `asListener(...)`.
## References
- https://www.laravelactions.com/2.x/as-listener.html

View file

@ -0,0 +1,118 @@
# Object Entrypoint (`run`, `make`, DI)
## Scope
Use this reference when the action is invoked as a plain object.
## Recap
- Explains object-style invocation with `make`, `run`, `runIf`, `runUnless`.
- Clarifies when to use static helpers versus DI/manual invocation.
- Includes minimal examples for direct run and service-level injection.
- Highlights boundaries: business logic stays in `handle(...)`.
## Recommended pattern
- Keep core business logic in `handle(...)`.
- Prefer `Action::run(...)` for readability.
- Use `Action::make()->handle(...)` or DI only when needed.
## Methods provided
### `make`
Resolves the action from the container.
```php
PublishArticle::make();
// Equivalent to:
app(PublishArticle::class);
```
### `run`
Resolves and executes the action.
```php
PublishArticle::run($articleId);
// Equivalent to:
PublishArticle::make()->handle($articleId);
```
### `runIf`
Resolves and executes the action only if the condition is met.
```php
PublishArticle::runIf($shouldPublish, $articleId);
// Equivalent mental model:
if ($shouldPublish) {
PublishArticle::run($articleId);
}
```
### `runUnless`
Resolves and executes the action only if the condition is not met.
```php
PublishArticle::runUnless($alreadyPublished, $articleId);
// Equivalent mental model:
if (! $alreadyPublished) {
PublishArticle::run($articleId);
}
```
## Checklist
- Input/output types are explicit.
- `handle(...)` has no transport concerns.
- Business behavior is covered by direct `handle(...)` tests.
## Common pitfalls
- Putting HTTP/CLI/queue concerns in `handle(...)`.
- Calling adapters from `handle(...)` instead of the reverse.
## References
- https://www.laravelactions.com/2.x/as-object.html
## Examples
### Minimal object-style invocation
```php
final class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
// Domain logic...
return true;
}
}
$published = PublishArticle::run(42);
```
### Dependency injection invocation
```php
final class ArticleService
{
public function __construct(
private PublishArticle $publishArticle
) {}
public function publish(int $articleId): bool
{
return $this->publishArticle->handle($articleId);
}
}
```

View file

@ -0,0 +1,160 @@
# Testing and Action Fakes
## Scope
Use this reference when isolating action orchestration in tests.
## Recap
- Summarizes all `AsFake` helpers (`mock`, `partialMock`, `spy`, `shouldRun`, `shouldNotRun`, `allowToRun`).
- Clarifies when to assert execution versus non-execution.
- Covers fake lifecycle checks/reset (`isFake`, `clearFake`).
- Provides branch-oriented test examples for orchestration confidence.
## Core methods
- `mock()`
- `partialMock()`
- `spy()`
- `shouldRun()`
- `shouldNotRun()`
- `allowToRun()`
- `isFake()`
- `clearFake()`
## Recommended pattern
- Test `handle(...)` directly for business rules.
- Test entrypoints for wiring/orchestration.
- Fake only at the boundary under test.
## Methods provided (`AsFake` trait)
### `mock`
Swaps the action with a full mock.
```php
FetchContactsFromGoogle::mock()
->shouldReceive('handle')
->with(42)
->andReturn(['Loris', 'Will', 'Barney']);
```
### `partialMock`
Swaps the action with a partial mock.
```php
FetchContactsFromGoogle::partialMock()
->shouldReceive('fetch')
->with('some_google_identifier')
->andReturn(['Loris', 'Will', 'Barney']);
```
### `spy`
Swaps the action with a spy.
```php
$spy = FetchContactsFromGoogle::spy()
->allows('handle')
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `shouldRun`
Helper adding expectation on `handle`.
```php
FetchContactsFromGoogle::shouldRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldReceive('handle');
```
### `shouldNotRun`
Helper adding negative expectation on `handle`.
```php
FetchContactsFromGoogle::shouldNotRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldNotReceive('handle');
```
### `allowToRun`
Helper allowing `handle` on a spy.
```php
$spy = FetchContactsFromGoogle::allowToRun()
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `isFake`
Returns whether the action has been swapped with a fake.
```php
FetchContactsFromGoogle::isFake(); // false
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
```
### `clearFake`
Clears the fake instance, if any.
```php
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
FetchContactsFromGoogle::clearFake();
FetchContactsFromGoogle::isFake(); // false
```
## Examples
### Orchestration test
```php
it('runs sync contacts for premium teams', function () {
SyncGoogleContacts::shouldRun()->once()->with(42)->andReturnTrue();
ImportTeamContacts::run(42, isPremium: true);
});
```
### Guard-clause test
```php
it('does not run sync when integration is disabled', function () {
SyncGoogleContacts::shouldNotRun();
ImportTeamContacts::run(42, integrationEnabled: false);
});
```
## Checklist
- Assertions verify call intent and argument contracts.
- Fakes are cleared when leakage risk exists.
- Branch tests use `shouldRun()` / `shouldNotRun()` where clearer.
## Common pitfalls
- Over-mocking and losing behavior confidence.
- Asserting only dispatch, not business correctness.
## References
- https://www.laravelactions.com/2.x/as-fake.html

View file

@ -0,0 +1,33 @@
# Troubleshooting
## Scope
Use this reference when action wiring behaves unexpectedly.
## Recap
- Provides a fast triage flow for routing, queueing, events, and command wiring.
- Lists recurring failure patterns and where to check first.
- Encourages reproducing issues with focused tests before broad debugging.
- Separates wiring diagnostics from domain logic verification.
## Fast checks
- Action class uses `AsAction`.
- Namespace and autoloading are correct.
- Entrypoint wiring (route, queue, event, command) is registered.
- Method signatures and argument types match caller expectations.
## Failure patterns
- Controller route points to wrong class.
- Queue worker/config mismatch.
- Listener mapping not loaded.
- Command signature mismatch.
- Command not registered in the console kernel.
## Debug checklist
- Reproduce with a focused failing test.
- Validate wiring layer first, then domain behavior.
- Isolate dependencies with fakes/spies where appropriate.

View file

@ -0,0 +1,189 @@
# With Attributes (`WithAttributes` trait)
## Scope
Use this reference when an action stores and validates input via internal attributes instead of method arguments.
## Recap
- Documents attribute lifecycle APIs (`setRawAttributes`, `fill`, `fillFromRequest`, readers/writers).
- Clarifies behavior of key collisions (`fillFromRequest`: request data wins over route params).
- Lists validation/authorization hooks reused from controller validation pipeline.
- Includes end-to-end example from fill to `validateAttributes()` and `handle(...)`.
## Methods provided (`WithAttributes` trait)
### `setRawAttributes`
Replaces all attributes with the provided payload.
```php
$action->setRawAttributes([
'key' => 'value',
]);
```
### `fill`
Merges provided attributes into existing attributes.
```php
$action->fill([
'key' => 'value',
]);
```
### `fillFromRequest`
Merges request input and route parameters into attributes. Request input has priority over route parameters when keys collide.
```php
$action->fillFromRequest($request);
```
### `all`
Returns all attributes.
```php
$action->all();
```
### `only`
Returns attributes matching the provided keys.
```php
$action->only('title', 'body');
```
### `except`
Returns attributes excluding the provided keys.
```php
$action->except('body');
```
### `has`
Returns whether an attribute exists for the given key.
```php
$action->has('title');
```
### `get`
Returns the attribute value by key, with optional default.
```php
$action->get('title');
$action->get('title', 'Untitled');
```
### `set`
Sets an attribute value by key.
```php
$action->set('title', 'My blog post');
```
### `__get`
Accesses attributes as object properties.
```php
$action->title;
```
### `__set`
Updates attributes as object properties.
```php
$action->title = 'My blog post';
```
### `__isset`
Checks attribute existence as object properties.
```php
isset($action->title);
```
### `validateAttributes`
Runs authorization and validation using action attributes and returns validated data.
```php
$validatedData = $action->validateAttributes();
```
## Methods used (`AttributeValidator`)
`WithAttributes` uses the same authorization/validation hooks as `AsController`:
- `prepareForValidation`
- `authorize`
- `rules`
- `withValidator`
- `afterValidator`
- `getValidator`
- `getValidationData`
- `getValidationMessages`
- `getValidationAttributes`
- `getValidationRedirect`
- `getValidationErrorBag`
- `getValidationFailure`
- `getAuthorizationFailure`
## Example
```php
class CreateArticle
{
use AsAction;
use WithAttributes;
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:8'],
'body' => ['required', 'string'],
];
}
public function handle(array $attributes): Article
{
return Article::create($attributes);
}
}
$action = CreateArticle::make()->fill([
'title' => 'My first post',
'body' => 'Hello world',
]);
$validated = $action->validateAttributes();
$article = $action->handle($validated);
```
## Checklist
- Attribute keys are explicit and stable.
- Validation rules match expected attribute shape.
- `validateAttributes()` is called before side effects when needed.
- Validation/authorization hooks are tested in focused unit tests.
## Common pitfalls
- Mixing attribute-based and argument-based flows inconsistently in the same action.
- Assuming route params override request input in `fillFromRequest` (they do not).
- Skipping `validateAttributes()` when using external input.
## References
- https://www.laravelactions.com/2.x/with-attributes.html

View file

@ -0,0 +1,190 @@
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
author: laravel
---
# Laravel Best Practices
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
## Consistency First
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
## Quick Reference
### 1. Database Performance → `rules/db-performance.md`
- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
### 3. Security → `rules/security.md`
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
### 4. Caching → `rules/caching.md`
- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production
### 5. Eloquent Patterns → `rules/eloquent.md`
- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
### 6. Validation & Forms → `rules/validation.md`
- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`
### 7. Configuration → `rules/config.md`
- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text
### 8. Testing Patterns → `rules/testing.md`
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios
### 10. Routing & Controllers → `rules/routing.md`
- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation
### 11. HTTP Client → `rules/http-client.md`
- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails
### 13. Error Handling → `rules/error-handling.md`
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes
### 14. Task Scheduling → `rules/scheduling.md`
- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration
### 15. Architecture → `rules/architecture.md`
- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
### 16. Migrations → `rules/migrations.md`
- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML
### 17. Collections → `rules/collections.md`
- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections
### 18. Blade & Views → `rules/blade-views.md`
- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props
### 19. Conventions & Style → `rules/style.md`
- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files
## How to Apply
Always use a sub-agent to read rule files and explore this skill's content.
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version

View file

@ -0,0 +1,106 @@
# Advanced Query Patterns
## Use `addSelect()` Subqueries for Single Values from Has-Many
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
```php
public function scopeWithLastLoginAt($query): void
{
$query->addSelect([
'last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->withCasts(['last_login_at' => 'datetime']);
}
```
## Create Dynamic Relationships via Subquery FK
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
```php
public function lastLogin(): BelongsTo
{
return $this->belongsTo(Login::class);
}
public function scopeWithLastLogin($query): void
{
$query->addSelect([
'last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}
```
## Use Conditional Aggregates Instead of Multiple Count Queries
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
```php
$statuses = Feature::toBase()
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
->first();
```
## Use `setRelation()` to Prevent Circular N+1
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
```php
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
```
## Prefer `whereIn` + Subquery Over `whereHas`
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
Incorrect (correlated EXISTS re-executes per row):
```php
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
```
Correct (index-friendly subquery, no PHP memory overhead):
```php
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
```
## Sometimes Two Simple Queries Beat One Complex Query
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
## Use Compound Indexes Matching `orderBy` Column Order
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
```php
// Migration
$table->index(['last_name', 'first_name']);
// Query — column order must match the index
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
```
## Use Correlated Subqueries for Has-Many Ordering
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
```php
public function scopeOrderByLastLogin($query): void
{
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
```

View file

@ -0,0 +1,202 @@
# Architecture Best Practices
## Single-Purpose Action Classes
Extract discrete business operations into invokable Action classes.
```php
class CreateOrderAction
{
public function __construct(private InventoryService $inventory) {}
public function execute(array $data): Order
{
$order = Order::create($data);
$this->inventory->reserve($order);
return $order;
}
}
```
## Use Dependency Injection
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
Incorrect:
```php
class OrderController extends Controller
{
public function store(StoreOrderRequest $request)
{
$service = app(OrderService::class);
return $service->create($request->validated());
}
}
```
Correct:
```php
class OrderController extends Controller
{
public function __construct(private OrderService $service) {}
public function store(StoreOrderRequest $request)
{
return $this->service->create($request->validated());
}
}
```
## Code to Interfaces
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
Incorrect (concrete dependency):
```php
class OrderService
{
public function __construct(private StripeGateway $gateway) {}
}
```
Correct (interface dependency):
```php
interface PaymentGateway
{
public function charge(int $amount, string $customerId): PaymentResult;
}
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
}
```
Bind in a service provider:
```php
$this->app->bind(PaymentGateway::class, StripeGateway::class);
```
## Default Sort by Descending
When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres.
Incorrect:
```php
$posts = Post::paginate();
```
Correct:
```php
$posts = Post::latest()->paginate();
```
## Use Atomic Locks for Race Conditions
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
```php
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
$order->process();
});
// Or at query level
$product = Product::where('id', $id)->lockForUpdate()->first();
```
## Use `mb_*` String Functions
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
Incorrect:
```php
strlen('José'); // 5 (bytes, not characters)
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
```
Correct:
```php
mb_strlen('José'); // 4 (characters)
mb_strtolower('MÜNCHEN'); // 'münchen'
// Prefer Laravel's Str helpers when available
Str::length('José'); // 4
Str::lower('MÜNCHEN'); // 'münchen'
```
## Use `defer()` for Post-Response Work
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
Incorrect (job overhead for trivial work):
```php
dispatch(new LogPageView($page));
```
Correct (runs after response, same process):
```php
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
```
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
## Use `Context` for Request-Scoped Data
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
```php
// In middleware
Context::add('tenant_id', $request->header('X-Tenant-ID'));
// Anywhere later — controllers, jobs, log context
$tenantId = Context::get('tenant_id');
```
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
## Use `Concurrency::run()` for Parallel Execution
Run independent operations in parallel using child processes — no async libraries needed.
```php
use Illuminate\Support\Facades\Concurrency;
[$users, $orders] = Concurrency::run([
fn () => User::count(),
fn () => Order::where('status', 'pending')->count(),
]);
```
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
## Convention Over Configuration
Follow Laravel conventions. Don't override defaults unnecessarily.
Incorrect:
```php
class Customer extends Model
{
protected $table = 'Customer';
protected $primaryKey = 'customer_id';
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
}
}
```
Correct:
```php
class Customer extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
```

View file

@ -0,0 +1,36 @@
# Blade & Views Best Practices
## Use `$attributes->merge()` in Component Templates
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
```blade
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
```
## Use `@pushOnce` for Per-Component Scripts
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
## Prefer Blade Components Over `@include`
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
## Use View Composers for Shared View Data
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
A single view can return either the full page or just a fragment, keeping routing clean.
```php
return view('dashboard', compact('users'))
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
```
## Use `@aware` for Deeply Nested Component Props
Avoids re-passing parent props through every level of nested components.

View file

@ -0,0 +1,70 @@
# Caching Best Practices
## Use `Cache::remember()` Instead of Manual Get/Put
Atomic pattern prevents race conditions and removes boilerplate.
Incorrect:
```php
$val = Cache::get('stats');
if (! $val) {
$val = $this->computeStats();
Cache::put('stats', $val, 60);
}
```
Correct:
```php
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
```
## Use `Cache::flexible()` for Stale-While-Revalidate
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
## Use Cache Tags to Invalidate Related Groups
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
```php
Cache::tags(['user-1'])->flush();
```
## Use `Cache::add()` for Atomic Conditional Writes
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
Correct: `Cache::add('lock', true, 10);`
## Use `once()` for Per-Request Memoization
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
```php
public function roles(): Collection
{
return once(fn () => $this->loadRoles());
}
```
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
## Configure Failover Cache Stores in Production
If Redis goes down, the app falls back to a secondary store automatically.
```php
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
```

View file

@ -0,0 +1,44 @@
# Collection Best Practices
## Use Higher-Order Messages for Simple Operations
Incorrect:
```php
$users->each(function (User $user) {
$user->markAsVip();
});
```
Correct: `$users->each->markAsVip();`
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
## Choose `cursor()` vs. `lazy()` Correctly
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
## Use `lazyById()` When Updating Records While Iterating
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
## Use `toQuery()` for Bulk Operations on Collections
Avoids manual `whereIn` construction.
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
Correct: `$users->toQuery()->update([...]);`
## Use `#[CollectedBy]` for Custom Collection Classes
More declarative than overriding `newCollection()`.
```php
#[CollectedBy(UserCollection::class)]
class User extends Model {}
```

View file

@ -0,0 +1,73 @@
# Configuration Best Practices
## `env()` Only in Config Files
Direct `env()` calls return `null` when config is cached.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'key' => env('API_KEY'),
// Application code
$key = config('services.key');
```
## Use Encrypted Env or External Secrets
Never store production secrets in plain `.env` files in version control.
Incorrect:
```bash
# .env committed to repo or shared in Slack
STRIPE_SECRET=sk_live_abc123
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
```
Correct:
```bash
php artisan env:encrypt --env=production --readable
php artisan env:decrypt --env=production
```
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
## Use `App::environment()` for Environment Checks
Incorrect:
```php
if (env('APP_ENV') === 'production') {
```
Correct:
```php
if (app()->isProduction()) {
// or
if (App::environment('production')) {
```
## Use Constants and Language Files
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
```php
// Incorrect
return $this->type === 'normal';
// Correct
return $this->type === self::TYPE_NORMAL;
```
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
```php
// Only when lang files already exist in the project
return back()->with('message', __('app.article_added'));
```

View file

@ -0,0 +1,192 @@
# Database Performance Best Practices
## Always Eager Load Relationships
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
Incorrect (N+1 — executes 1 + N queries):
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Correct (2 queries total):
```php
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Constrain eager loads to select only needed columns (always include the foreign key):
```php
$users = User::with(['posts' => function ($query) {
$query->select('id', 'user_id', 'title')
->where('published', true)
->latest()
->limit(10);
}])->get();
```
## Prevent Lazy Loading in Development
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
```php
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
```
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
## Select Only Needed Columns
Avoid `SELECT *` — especially when tables have large text or JSON columns.
Incorrect:
```php
$posts = Post::with('author')->get();
```
Correct:
```php
$posts = Post::select('id', 'title', 'user_id', 'created_at')
->with(['author:id,name,avatar'])
->get();
```
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
## Chunk Large Datasets
Never load thousands of records at once. Use chunking for batch processing.
Incorrect:
```php
$users = User::all();
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
```
Correct:
```php
User::where('subscribed', true)->chunk(200, function ($users) {
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
});
```
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
```php
User::where('active', false)->chunkById(200, function ($users) {
$users->each->delete();
});
```
## Add Database Indexes
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index()->constrained();
$table->string('status')->index();
$table->timestamps();
$table->index(['status', 'created_at']);
});
```
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
## Use `withCount()` for Counting Relations
Never load entire collections just to count them.
Incorrect:
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
```
Correct:
```php
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
```
Conditional counting:
```php
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => function ($query) {
$query->where('approved', true);
},
])->get();
```
## Use `cursor()` for Memory-Efficient Iteration
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
Incorrect:
```php
$users = User::where('active', true)->get();
```
Correct:
```php
foreach (User::where('active', true)->cursor() as $user) {
ProcessUser::dispatch($user->id);
}
```
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
## No Queries in Blade Templates
Never execute queries in Blade templates. Pass data from controllers.
Incorrect:
```blade
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
```
Correct:
```php
// Controller
$users = User::with('profile')->get();
return view('users.index', compact('users'));
```
```blade
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
```

View file

@ -0,0 +1,148 @@
# Eloquent Best Practices
## Use Correct Relationship Types
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
```php
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
```
## Use Local Scopes for Reusable Queries
Extract reusable query constraints into local scopes to avoid duplication.
Incorrect:
```php
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
$articles = Article::whereHas('user', function ($q) {
$q->where('verified', true)->whereNotNull('activated_at');
})->get();
```
Correct:
```php
public function scopeActive(Builder $query): Builder
{
return $query->where('verified', true)->whereNotNull('activated_at');
}
// Usage
$active = User::active()->get();
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
```
## Apply Global Scopes Sparingly
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
Incorrect (global scope for a conditional filter):
```php
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('published', true);
}
}
// Now admin panels, reports, and background jobs all silently skip drafts
```
Correct (local scope you opt into):
```php
public function scopePublished(Builder $query): Builder
{
return $query->where('published', true);
}
Post::published()->paginate(); // Explicit
Post::paginate(); // Admin sees all
```
## Define Attribute Casts
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
```php
protected function casts(): array
{
return [
'is_active' => 'boolean',
'metadata' => 'array',
'total' => 'decimal:2',
];
}
```
## Cast Date Columns Properly
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
Incorrect:
```blade
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
```
Correct:
```php
protected function casts(): array
{
return [
'ordered_at' => 'datetime',
];
}
```
```blade
{{ $order->ordered_at->toDateString() }}
{{ $order->ordered_at->format('m-d') }}
```
## Use `whereBelongsTo()` for Relationship Queries
Cleaner than manually specifying foreign keys.
Incorrect:
```php
Post::where('user_id', $user->id)->get();
```
Correct:
```php
Post::whereBelongsTo($user)->get();
Post::whereBelongsTo($user, 'author')->get();
```
## Avoid Hardcoded Table Names in Queries
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
Incorrect:
```php
DB::table('users')->where('active', true)->get();
$query->join('companies', 'companies.id', '=', 'users.company_id');
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
```
Correct — reference the model's table:
```php
DB::table((new User)->getTable())->where('active', true)->get();
// Even better — use Eloquent or the query builder instead of raw SQL
User::where('active', true)->get();
Order::where('status', 'pending')->get();
```
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.

View file

@ -0,0 +1,72 @@
# Error Handling Best Practices
## Exception Reporting and Rendering
There are two valid approaches — choose one and apply it consistently across the project.
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
```php
class InvalidOrderException extends Exception
{
public function report(): void { /* custom reporting */ }
public function render(Request $request): Response
{
return response()->view('errors.invalid-order', status: 422);
}
}
```
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
```php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
$exceptions->render(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', status: 422);
});
})
```
Check the existing codebase and follow whichever pattern is already established.
## Use `ShouldntReport` for Exceptions That Should Never Log
More discoverable than listing classes in `dontReport()`.
```php
class PodcastProcessingException extends Exception implements ShouldntReport {}
```
## Throttle High-Volume Exceptions
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
## Enable `dontReportDuplicates()`
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
## Force JSON Error Rendering for API Routes
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
```php
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
return $request->is('api/*') || $request->expectsJson();
});
```
## Add Context to Exception Classes
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
```php
class InvalidOrderException extends Exception
{
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
```

View file

@ -0,0 +1,48 @@
# Events & Notifications Best Practices
## Rely on Event Discovery
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
## Run `event:cache` in Production Deploy
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
## Use `ShouldDispatchAfterCommit` Inside Transactions
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
```php
class OrderShipped implements ShouldDispatchAfterCommit {}
```
## Always Queue Notifications
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
```php
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
}
```
## Use `afterCommit()` on Notifications in Transactions
Same race condition as events — the queued notification job may run before the transaction commits.
## Route Notification Channels to Dedicated Queues
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
## Use On-Demand Notifications for Non-User Recipients
Avoid creating dummy models to send notifications to arbitrary addresses.
```php
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
```
## Implement `HasLocalePreference` on Notifiable Models
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.

View file

@ -0,0 +1,160 @@
# HTTP Client Best Practices
## Always Set Explicit Timeouts
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
Incorrect:
```php
$response = Http::get('https://api.example.com/users');
```
Correct:
```php
$response = Http::timeout(5)
->connectTimeout(3)
->get('https://api.example.com/users');
```
For service-specific clients, define timeouts in a macro:
```php
Http::macro('github', function () {
return Http::baseUrl('https://api.github.com')
->timeout(10)
->connectTimeout(3)
->withToken(config('services.github.token'));
});
$response = Http::github()->get('/repos/laravel/framework');
```
## Use Retry with Backoff for External APIs
External APIs have transient failures. Use `retry()` with increasing delays.
Incorrect:
```php
$response = Http::post('https://api.stripe.com/v1/charges', $data);
if ($response->failed()) {
throw new PaymentFailedException('Charge failed');
}
```
Correct:
```php
$response = Http::retry([100, 500, 1000])
->timeout(10)
->post('https://api.stripe.com/v1/charges', $data);
```
Only retry on specific errors:
```php
$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {
return $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError());
})->post('https://api.example.com/data');
```
## Handle Errors Explicitly
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
Incorrect:
```php
$response = Http::get('https://api.example.com/users/1');
$user = $response->json(); // Could be an error body
```
Correct:
```php
$response = Http::timeout(5)
->get('https://api.example.com/users/1')
->throw();
$user = $response->json();
```
For graceful degradation:
```php
$response = Http::get('https://api.example.com/users/1');
if ($response->successful()) {
return $response->json();
}
if ($response->notFound()) {
return null;
}
$response->throw();
```
## Use Request Pooling for Concurrent Requests
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
Incorrect:
```php
$users = Http::get('https://api.example.com/users')->json();
$posts = Http::get('https://api.example.com/posts')->json();
$comments = Http::get('https://api.example.com/comments')->json();
```
Correct:
```php
use Illuminate\Http\Client\Pool;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('users')->get('https://api.example.com/users'),
$pool->as('posts')->get('https://api.example.com/posts'),
$pool->as('comments')->get('https://api.example.com/comments'),
]);
$users = $responses['users']->json();
$posts = $responses['posts']->json();
```
## Fake HTTP Calls in Tests
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
Incorrect:
```php
it('syncs user from API', function () {
$service = new UserSyncService;
$service->sync(1); // Hits the real API
});
```
Correct:
```php
it('syncs user from API', function () {
Http::preventStrayRequests();
Http::fake([
'api.example.com/users/1' => Http::response([
'name' => 'John Doe',
'email' => 'john@example.com',
]),
]);
$service = new UserSyncService;
$service->sync(1);
Http::assertSent(function (Request $request) {
return $request->url() === 'https://api.example.com/users/1';
});
});
```
Test failure scenarios too:
```php
Http::fake([
'api.example.com/*' => Http::failedConnection(),
]);
```

View file

@ -0,0 +1,27 @@
# Mail Best Practices
## Implement `ShouldQueue` on the Mailable Class
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
## Use `afterCommit()` on Mailables Inside Transactions
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence.
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
Correct: `Mail::assertQueued(OrderShipped::class);`
## Use Markdown Mailables for Transactional Emails
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
## Separate Content Tests from Sending Tests
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
Don't mix them — it conflates concerns and makes tests brittle.

View file

@ -0,0 +1,121 @@
# Migration Best Practices
## Generate Migrations with Artisan
Always use `php artisan make:migration` for consistent naming and timestamps.
Incorrect (manually created file):
```php
// database/migrations/posts_migration.php ← wrong naming, no timestamp
```
Correct (Artisan-generated):
```bash
php artisan make:migration create_posts_table
php artisan make:migration add_slug_to_posts_table
```
## Use `constrained()` for Foreign Keys
Automatic naming and referential integrity.
```php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
// Non-standard names
$table->foreignId('author_id')->constrained('users');
```
## Never Modify Deployed Migrations
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
Incorrect (editing a deployed migration):
```php
// 2024_01_01_create_posts_table.php — already in production
$table->string('slug')->unique(); // ← added after deployment
```
Correct (new migration to alter):
```php
// 2024_03_15_add_slug_to_posts_table.php
Schema::table('posts', function (Blueprint $table) {
$table->string('slug')->unique()->after('title');
});
```
## Add Indexes in the Migration
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->index();
$table->string('status')->index();
$table->timestamp('shipped_at')->nullable()->index();
$table->timestamps();
});
```
## Mirror Defaults in Model `$attributes`
When a column has a database default, mirror it in the model so new instances have correct values before saving.
```php
// Migration
$table->string('status')->default('pending');
// Model
protected $attributes = [
'status' => 'pending',
];
```
## Write Reversible `down()` Methods by Default
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
```php
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
```
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
## Keep Migrations Focused
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
Incorrect (partial failure creates unrecoverable state):
```php
public function up(): void
{
Schema::create('settings', function (Blueprint $table) { ... });
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
}
```
Correct (separate migrations):
```php
// Migration 1: create_settings_table
Schema::create('settings', function (Blueprint $table) { ... });
// Migration 2: seed_default_settings
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
```

View file

@ -0,0 +1,146 @@
# Queue & Job Best Practices
## Set `retry_after` Greater Than `timeout`
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
Incorrect (`retry_after` ≤ `timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 90 ← job retried while still running!
```
Correct (`retry_after` > `timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
```
## Use Exponential Backoff
Use progressively longer delays between retries to avoid hammering failing services.
Incorrect (fixed retry interval):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
// Default: retries immediately, overwhelming the API
}
```
Correct (exponential backoff):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
public $backoff = [1, 5, 10];
}
```
## Implement `ShouldBeUnique`
Prevent duplicate job processing.
```php
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->order->id;
}
public $uniqueFor = 3600;
}
```
## Always Implement `failed()`
Handle errors explicitly — don't rely on silent failure.
```php
public function failed(?Throwable $exception): void
{
$this->podcast->update(['status' => 'failed']);
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
}
```
## Rate Limit External API Calls in Jobs
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
```php
public function middleware(): array
{
return [new RateLimited('external-api')];
}
```
## Batch Related Jobs
Use `Bus::batch()` when jobs should succeed or fail together.
```php
Bus::batch([
new ImportCsvChunk($chunk1),
new ImportCsvChunk($chunk2),
])
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
->dispatch();
```
## `retryUntil()` Needs `$tries = 0`
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
```php
public $tries = 0;
public function retryUntil(): DateTime
{
return now()->addHours(4);
}
```
## Use `WithoutOverlapping::untilProcessing()`
Prevents concurrent execution while allowing new instances to queue.
```php
public function middleware(): array
{
return [new WithoutOverlapping($this->product->id)->untilProcessing()];
}
```
Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts.
## Use Horizon for Complex Queue Scenarios
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
```php
// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
],
```

View file

@ -0,0 +1,98 @@
# Routing & Controllers Best Practices
## Use Implicit Route Model Binding
Let Laravel resolve models automatically from route parameters.
Incorrect:
```php
public function show(int $id)
{
$post = Post::findOrFail($id);
}
```
Correct:
```php
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
```
## Use Scoped Bindings for Nested Resources
Enforce parent-child relationships automatically.
```php
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
// $post is automatically scoped to $user
})->scopeBindings();
```
## Use Resource Controllers
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
```php
Route::resource('posts', PostController::class);
Route::apiResource('api/posts', Api\PostController::class);
```
## Keep Controllers Thin
Aim for under 10 lines per method. Extract business logic to action or service classes.
Incorrect:
```php
public function store(Request $request)
{
$validated = $request->validate([...]);
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images'));
}
$post = Post::create($validated);
$post->tags()->sync($validated['tags']);
event(new PostCreated($post));
return redirect()->route('posts.show', $post);
}
```
Correct:
```php
public function store(StorePostRequest $request, CreatePostAction $create)
{
$post = $create->execute($request->validated());
return redirect()->route('posts.show', $post);
}
```
## Type-Hint Form Requests
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
Incorrect:
```php
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'max:255'],
'body' => ['required'],
]);
Post::create($validated);
return redirect()->route('posts.index');
}
```
Correct:
```php
public function store(StorePostRequest $request): RedirectResponse
{
Post::create($request->validated());
return redirect()->route('posts.index');
}
```

View file

@ -0,0 +1,39 @@
# Task Scheduling Best Practices
## Use `withoutOverlapping()` on Variable-Duration Tasks
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
## Use `onOneServer()` on Multi-Server Deployments
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
## Use `runInBackground()` for Concurrent Long Tasks
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
## Use `environments()` to Restrict Tasks
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
```php
Schedule::command('billing:charge')->monthly()->environments(['production']);
```
## Use `takeUntilTimeout()` for Time-Bounded Processing
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
## Use Schedule Groups for Shared Configuration
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
```php
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
```

View file

@ -0,0 +1,198 @@
# Security Best Practices
## Mass Assignment Protection
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
Incorrect:
```php
class User extends Model
{
protected $guarded = []; // All fields are mass assignable
}
```
Correct:
```php
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
```
Never use `$guarded = []` on models that accept user input.
## Authorize Every Action
Use policies or gates in controllers. Never skip authorization.
Incorrect:
```php
public function update(Request $request, Post $post)
{
$post->update($request->validated());
}
```
Correct:
```php
public function update(UpdatePostRequest $request, Post $post)
{
Gate::authorize('update', $post);
$post->update($request->validated());
}
```
Or via Form Request:
```php
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
```
## Prevent SQL Injection
Always use parameter binding. Never interpolate user input into queries.
Incorrect:
```php
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
```
Correct:
```php
User::where('name', $request->name)->get();
// Raw expressions with bindings
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
```
## Escape Output to Prevent XSS
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
Incorrect:
```blade
{!! $user->bio !!}
```
Correct:
```blade
{{ $user->bio }}
```
## CSRF Protection
Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia.
Incorrect:
```blade
<form method="POST" action="/posts">
<input type="text" name="title">
</form>
```
Correct:
```blade
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
</form>
```
## Rate Limit Auth and API Routes
Apply `throttle` middleware to authentication and API routes.
```php
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Route::post('/login', LoginController::class)->middleware('throttle:login');
```
## Validate File Uploads
Validate MIME type, extension, and size. Never trust client-provided filenames.
```php
public function rules(): array
{
return [
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
];
}
```
Store with generated filenames:
```php
$path = $request->file('avatar')->store('avatars', 'public');
```
## Keep Secrets Out of Code
Never commit `.env`. Access secrets via `config()` only.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'api_key' => env('API_KEY'),
// In application code
$key = config('services.api_key');
```
## Audit Dependencies
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
```bash
composer audit
```
## Encrypt Sensitive Database Fields
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
Incorrect:
```php
class Integration extends Model
{
protected function casts(): array
{
return [
'api_key' => 'string',
];
}
}
```
Correct:
```php
class Integration extends Model
{
protected $hidden = ['api_key', 'api_secret'];
protected function casts(): array
{
return [
'api_key' => 'encrypted',
'api_secret' => 'encrypted',
];
}
}
```

Binary file not shown.

View file

@ -0,0 +1,43 @@
# Testing Best Practices
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites.
## Use Model Assertions Over Raw Database Assertions
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
Correct: `$this->assertModelExists($user);`
More expressive, type-safe, and fails with clearer messages.
## Use Factory States and Sequences
Named states make tests self-documenting. Sequences eliminate repetitive setup.
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
Correct: `User::factory()->unverified()->create();`
## Use `Exceptions::fake()` to Assert Exception Reporting
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
## Call `Event::fake()` After Factory Setup
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
Incorrect: `Event::fake(); $user = User::factory()->create();`
Correct: `$user = User::factory()->create(); Event::fake();`
## Use `recycle()` to Share Relationship Instances Across Factories
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
```php
Ticket::factory()
->recycle(Airline::factory()->create())
->create();
```

View file

@ -0,0 +1,75 @@
# Validation & Forms Best Practices
## Use Form Request Classes
Extract validation from controllers into dedicated Form Request classes.
Incorrect:
```php
public function store(Request $request)
{
$request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
}
```
Correct:
```php
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
```
## Array vs. String Notation for Rules
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
```php
// Preferred for new code
'email' => ['required', 'email', Rule::unique('users')],
// Follow existing convention if the project uses string notation
'email' => 'required|email|unique:users',
```
## Always Use `validated()`
Get only validated data. Never use `$request->all()` for mass operations.
Incorrect:
```php
Post::create($request->all());
```
Correct:
```php
Post::create($request->validated());
```
## Use `Rule::when()` for Conditional Validation
```php
'company_name' => [
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
],
```
## Use the `after()` Method for Custom Validation
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
```php
public function after(): array
{
return [
function (Validator $validator) {
if ($this->quantity > Product::find($this->product_id)?->stock) {
$validator->errors()->add('quantity', 'Not enough stock.');
}
},
];
}
```

View file

@ -1,24 +1,13 @@
---
name: livewire-development
description: >-
Develops reactive Livewire 3 components. Activates when creating, updating, or modifying
Livewire components; working with wire:model, wire:click, wire:loading, or any wire: directives;
adding real-time updates, loading states, or reactivity; debugging component behavior;
writing Livewire tests; or when the user mentions Livewire, component, counter, or reactive UI.
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, loading states, migrating from Livewire 2 to 3, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
license: MIT
metadata:
author: laravel
---
# Livewire Development
## When to Apply
Activate this skill when:
- Creating new Livewire components
- Modifying existing component state or behavior
- Debugging reactivity or lifecycle issues
- Writing Livewire component tests
- Adding Alpine.js interactivity to components
- Working with wire: directives
## Documentation
Use `search-docs` for detailed Livewire 3 patterns and documentation.
@ -62,33 +51,31 @@ ### Component Structure
### Using Keys in Loops
<code-snippet name="Wire Key in Loops" lang="blade">
<!-- Wire Key in Loops -->
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
</code-snippet>
```
### Lifecycle Hooks
Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle Hook Examples" lang="php">
<!-- Lifecycle Hook Examples -->
```php
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
```
## JavaScript Hooks
You can listen for `livewire:init` to hook into Livewire initialization:
<code-snippet name="Livewire Init Hook Example" lang="js">
<!-- Livewire Init Hook Example -->
```js
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
@ -100,28 +87,25 @@ ## JavaScript Hooks
console.error(message);
});
});
</code-snippet>
```
## Testing
<code-snippet name="Example Livewire Component Test" lang="php">
<!-- Example Livewire Component Test -->
```php
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
```
</code-snippet>
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
<!-- Testing Livewire Component Exists on Page -->
```php
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
```
## Common Pitfalls

View file

@ -1,24 +1,13 @@
---
name: pest-testing
description: >-
Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature
tests, adding assertions, testing Livewire components, browser testing, debugging test failures,
working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion,
coverage, or needs to verify functionality works.
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
@ -37,13 +26,12 @@ ### Test Organization
### Basic Test Structure
<code-snippet name="Basic Pest Test Example" lang="php">
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
```
### Running Tests
@ -55,13 +43,12 @@ ## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<code-snippet name="Pest Response Assertion" lang="php">
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
</code-snippet>
```
| Use | Instead of |
|-----|------------|
@ -77,16 +64,15 @@ ## Datasets
Use datasets for repetitive tests (validation rules, etc.):
<code-snippet name="Pest Dataset Example" lang="php">
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
```
## Pest 4 Features
@ -111,8 +97,8 @@ ### Browser Test Example
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<code-snippet name="Pest Browser Test Example" lang="php">
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
@ -129,20 +115,18 @@ ### Browser Test Example
Notification::assertSent(ResetPassword::class);
});
</code-snippet>
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<code-snippet name="Pest Smoke Testing Example" lang="php">
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
</code-snippet>
```
### Visual Regression Testing
@ -156,14 +140,13 @@ ### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<code-snippet name="Architecture Test Example" lang="php">
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
</code-snippet>
```
## Common Pitfalls

View file

@ -0,0 +1,80 @@
---
name: socialite-development
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
license: MIT
metadata:
author: laravel
---
# Socialite Authentication
## Documentation
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
## Available Providers
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
Community providers differ from built-in providers in the following ways:
- Installed via `composer require socialiteproviders/{name}`
- Must register via event listener — NOT auto-discovered like built-in providers
- Use `search-docs` for the registration pattern
## Adding a Provider
### 1. Configure the provider
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
### 2. Create redirect and callback routes
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
### 3. Authenticate and store the user
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
### 4. Customize the redirect (optional)
- `scopes()` — merge additional scopes with the provider's defaults
- `setScopes()` — replace all scopes entirely
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
- `stateless()` — for API/SPA contexts where session state is not maintained
### 5. Verify
1. Config key matches driver name exactly (check the list above for hyphenated names)
2. `client_id`, `client_secret`, and `redirect` are all present
3. Redirect URL matches what is registered in the provider's OAuth dashboard
4. Callback route handles denied grants (when user declines authorization)
Use `search-docs` for complete code examples of each step.
## Additional Features
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
## Testing
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
## Common Pitfalls
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
- Community providers require event listener registration via `SocialiteWasCalled`.
- `user()` throws when the user declines authorization. Always handle denied grants.

View file

@ -1,24 +1,13 @@
---
name: tailwindcss-development
description: >-
Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components,
working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors,
typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle,
hero section, cards, buttons, or any visual/UI changes.
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
author: laravel
---
# Tailwind CSS Development
## When to Apply
Activate this skill when:
- Adding styles to components or pages
- Working with responsive design
- Implementing dark mode
- Extracting repeated patterns into components
- Debugging spacing or layout issues
## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
@ -38,22 +27,24 @@ ### CSS-First Configuration
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<code-snippet name="CSS-First Config" lang="css">
<!-- CSS-First Config -->
```css
@theme {
--color-brand: oklch(0.72 0.11 178);
}
</code-snippet>
```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<code-snippet name="v4 Import Syntax" lang="diff">
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
```
### Replaced Utilities
@ -77,43 +68,47 @@ ## Spacing
Use `gap` utilities instead of margins for spacing between siblings:
<code-snippet name="Gap Utilities" lang="html">
<!-- Gap Utilities -->
```html
<div class="flex gap-8">
<div>Item 1</div>
<div>Item 2</div>
</div>
</code-snippet>
```
## Dark Mode
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
<code-snippet name="Dark Mode" lang="html">
<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content adapts to color scheme
</div>
</code-snippet>
```
## Common Patterns
### Flexbox Layout
<code-snippet name="Flexbox Layout" lang="html">
<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
<div>Left content</div>
<div>Right content</div>
</div>
</code-snippet>
```
### Grid Layout
<code-snippet name="Grid Layout" lang="html">
<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
</code-snippet>
```
## Common Pitfalls

View file

@ -0,0 +1,85 @@
---
name: configuring-horizon
description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching."
license: MIT
metadata:
author: laravel
---
# Horizon Configuration
## Documentation
Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment.
For deeper guidance on specific topics, read the relevant reference file before implementing:
- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling
- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config
- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs
- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config
## Basic Usage
### Installation
```bash
php artisan horizon:install
```
### Supervisor Configuration
Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block:
<!-- Supervisor Config -->
```php
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
'environments' => [
'production' => [
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
],
'local' => [
'supervisor-1' => ['maxProcesses' => 2],
],
],
```
### Dashboard Authorization
Restrict access in `App\Providers\HorizonServiceProvider`:
<!-- Dashboard Gate -->
```php
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
return $user->is_admin;
});
}
```
## Verification
1. Run `php artisan horizon` and visit `/horizon`
2. Confirm dashboard access is restricted as expected
3. Check that metrics populate after scheduling `horizon:snapshot`
## Common Pitfalls
- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported.
- Redis Cluster is not supported. Horizon requires a standalone Redis connection.
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.

View file

@ -0,0 +1,21 @@
# Metrics & Snapshots
## Where to Find It
Search with `search-docs`:
- `"horizon metrics snapshot"` for the snapshot command and scheduling
- `"horizon trim snapshots"` for retention configuration
## What to Watch For
### Metrics dashboard stays blank until `horizon:snapshot` is scheduled
Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler.
### Register the snapshot in the scheduler rather than running it manually
A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+.
### `metrics.trim_snapshots` is a snapshot count, not a time duration
The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage.

View file

@ -0,0 +1,21 @@
# Notifications & Alerts
## Where to Find It
Search with `search-docs`:
- `"horizon notifications"` for Horizon's built-in notification routing helpers
- `"horizon long wait detected"` for LongWaitDetected event details
## What to Watch For
### `waits` in `config/horizon.php` controls the LongWaitDetected threshold
The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration.
### Use Horizon's built-in notification routing in `HorizonServiceProvider`
Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration.
### Failed job alerts are separate from Horizon's documented notification routing
Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API.

View file

@ -0,0 +1,27 @@
# Supervisor & Balancing Configuration
## Where to Find It
Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions:
- `"horizon supervisor configuration"` for the full options list
- `"horizon balancing strategies"` for auto, simple, and false modes
- `"horizon autoscaling workers"` for autoScalingStrategy details
- `"horizon environment configuration"` for the defaults and environments merge
## What to Watch For
### The `environments` array merges into `defaults` rather than replacing it
The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`.
### Use separate named supervisors to enforce queue priority
Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this.
### Use `balance: false` to keep a fixed number of workers on a dedicated queue
Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable.
### Set `balanceCooldown` to prevent rapid worker scaling under bursty load
When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle.

View file

@ -0,0 +1,21 @@
# Tags & Silencing
## Where to Find It
Search with `search-docs`:
- `"horizon tags"` for the tagging API and auto-tagging behaviour
- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options
## What to Watch For
### Eloquent model jobs are tagged automatically without any extra code
If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed.
### `silenced` hides jobs from the dashboard completed list but does not stop them from running
Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs.
### `silenced_tags` hides all jobs carrying a matching tag from the completed list
Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes.

View file

@ -1,6 +1,9 @@
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
name: fortify-development
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
license: MIT
metadata:
author: laravel
---
# Laravel Fortify Development
@ -39,7 +42,7 @@ ### Two-Factor Authentication Setup
```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
@ -75,14 +78,26 @@ ### SPA Authentication Setup
```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```
> Use `search-docs` for integration and SPA authentication patterns.
#### Two-Factor Authentication in SPA Mode
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
```json
{
"two_factor": true
}
```
## Best Practices
### Custom Authentication Logic

View file

@ -0,0 +1,302 @@
---
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.
---
# Laravel Actions or `lorisleiva/laravel-actions`
## Overview
Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns.
## Quick Workflow
1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`.
2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`.
3. Implement `handle(...)` with the core business logic first.
4. Add adapter methods only when needed for the requested entrypoint:
- `asController` (+ route/invokable controller usage)
- `asJob` (+ dispatch)
- `asListener` (+ event listener wiring)
- `asCommand` (+ command signature/description)
5. Add or update tests for the chosen entrypoint.
6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`).
## Base Action Pattern
Use this minimal skeleton and expand only what is needed.
```php
<?php
namespace App\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
return true;
}
}
```
## Project Conventions
- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used.
- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`).
- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`).
- Prefer explicit parameter and return types in all action methods.
- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments.
### When to Use an Action
- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking.
- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action.
## Entrypoint Patterns
### Run as Object
- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`.
- Use make and call handle: `PublishArticle::make()->handle($id)`.
- Call with dependency injection: `app(PublishArticle::class)->handle($id)`.
### Run as Controller
- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`.
- Add `asController(...)` for HTTP-specific adaptation and return a response.
- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP.
### Run as Job
- Dispatch with `PublishArticle::dispatch($id)`.
- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`.
- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control.
#### Project Pattern: Job Action with Extra Methods
```php
<?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.
### Run as Listener
- Register the action class as listener in `EventServiceProvider`.
- Use `asListener(EventName $event)` and delegate to `handle(...)`.
### Run as Command
- Define `$commandSignature` and `$commandDescription` properties.
- Implement `asCommand(Command $command)` and keep console IO in this method only.
- Import `Command` with `use Illuminate\Console\Command;`.
## Testing Guidance
Use a two-layer strategy:
1. `handle(...)` tests for business correctness.
2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration.
### Deep Dive: `AsFake` methods (2.x)
Reference: https://www.laravelactions.com/2.x/as-fake.html
Use these methods intentionally based on what you want to prove.
#### `mock()`
- Replaces the action with a full mock.
- Best when you need strict expectations and argument assertions.
```php
PublishArticle::mock()
->shouldReceive('handle')
->once()
->with(42)
->andReturnTrue();
```
#### `partialMock()`
- Replaces the action with a partial mock.
- Best when you want to keep most real behavior but stub one expensive/internal method.
```php
PublishArticle::partialMock()
->shouldReceive('fetchRemoteData')
->once()
->andReturn(['ok' => true]);
```
#### `spy()`
- Replaces the action with a spy.
- Best for post-execution verification ("was called with X") without predefining all expectations.
```php
$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
// execute code that triggers the action...
$spy->shouldHaveReceived('handle')->with(42);
```
#### `shouldRun()`
- Shortcut for `mock()->shouldReceive('handle')`.
- Best for compact orchestration assertions.
```php
PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
```
#### `shouldNotRun()`
- Shortcut for `mock()->shouldNotReceive('handle')`.
- Best for guard-clause tests and branch coverage.
```php
PublishArticle::shouldNotRun();
```
#### `allowToRun()`
- Shortcut for spy + allowing `handle`.
- Best when you want execution to proceed but still assert interaction.
```php
$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.
```php
expect(PublishArticle::isFake())->toBeFalse();
PublishArticle::mock();
expect(PublishArticle::isFake())->toBeTrue();
PublishArticle::clearFake();
expect(PublishArticle::isFake())->toBeFalse();
```
### Recommended test matrix for Actions
- Business rule test: call `handle(...)` directly with real dependencies/factories.
- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`.
- Job wiring test: dispatch action as job, assert expected downstream action calls.
- Event listener test: dispatch event, assert action interaction via fake/spy.
- Console test: run artisan command, assert action invocation and output.
### Practical defaults
- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests.
- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification.
- Prefer `mock()` when interaction contracts are strict and should fail fast.
- Use `clearFake()` in cleanup when a fake might leak into another test.
- Keep side effects isolated: fake only the action under test boundary, not everything.
### Pest style examples
```php
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.
## Troubleshooting Checklist
- Ensure the class uses `AsAction` and namespace matches autoload.
- Check route registration when used as controller.
- Check queue config when using `dispatch`.
- Verify event-to-listener mapping in `EventServiceProvider`.
- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`.
## Common Pitfalls
- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`.
- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`.
- Assuming listener wiring works without explicit registration where required.
- Testing only entrypoints and skipping direct `handle(...)` behavior tests.
- Overusing Actions for one-off, single-context logic with no reuse pressure.
## Topic References
Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules.
- Object entrypoint: `references/object.md`
- Controller entrypoint: `references/controller.md`
- Job entrypoint: `references/job.md`
- Listener entrypoint: `references/listener.md`
- Command entrypoint: `references/command.md`
- With attributes: `references/with-attributes.md`
- Testing and fakes: `references/testing-fakes.md`
- Troubleshooting: `references/troubleshooting.md`

View file

@ -0,0 +1,160 @@
# Command Entrypoint (`asCommand`)
## Scope
Use this reference when exposing actions as Artisan commands.
## Recap
- Documents command execution via `asCommand(...)` and fallback to `handle(...)`.
- Covers command metadata via methods/properties (signature, description, help, hidden).
- Includes registration example and focused artisan test pattern.
- Reinforces separation between console I/O and domain logic.
## Recommended pattern
- Define `$commandSignature` and `$commandDescription`.
- Implement `asCommand(Command $command)` for console I/O.
- Keep business logic in `handle(...)`.
## Methods used (`CommandDecorator`)
### `asCommand`
Called when executed as a command. If missing, it falls back to `handle(...)`.
```php
use Illuminate\Console\Command;
class UpdateUserRole
{
use AsAction;
public string $commandSignature = 'users:update-role {user_id} {role}';
public function handle(User $user, string $newRole): void
{
$user->update(['role' => $newRole]);
}
public function asCommand(Command $command): void
{
$this->handle(
User::findOrFail($command->argument('user_id')),
$command->argument('role')
);
$command->info('Done!');
}
}
```
### `getCommandSignature`
Defines the command signature. Required when registering an action as a command if no `$commandSignature` property is set.
```php
public function getCommandSignature(): string
{
return 'users:update-role {user_id} {role}';
}
```
### `$commandSignature`
Property alternative to `getCommandSignature`.
```php
public string $commandSignature = 'users:update-role {user_id} {role}';
```
### `getCommandDescription`
Provides command description.
```php
public function getCommandDescription(): string
{
return 'Updates the role of a given user.';
}
```
### `$commandDescription`
Property alternative to `getCommandDescription`.
```php
public string $commandDescription = 'Updates the role of a given user.';
```
### `getCommandHelp`
Provides additional help text shown with `--help`.
```php
public function getCommandHelp(): string
{
return 'My help message.';
}
```
### `$commandHelp`
Property alternative to `getCommandHelp`.
```php
public string $commandHelp = 'My help message.';
```
### `isCommandHidden`
Defines whether command should be hidden from artisan list. Default is `false`.
```php
public function isCommandHidden(): bool
{
return true;
}
```
### `$commandHidden`
Property alternative to `isCommandHidden`.
```php
public bool $commandHidden = true;
```
## Examples
### Register in console kernel
```php
// app/Console/Kernel.php
protected $commands = [
UpdateUserRole::class,
];
```
### Focused command test
```php
$this->artisan('users:update-role 1 admin')
->expectsOutput('Done!')
->assertSuccessful();
```
## Checklist
- `use Illuminate\Console\Command;` is imported.
- Signature/options/arguments are documented.
- Command test verifies invocation and output.
## Common pitfalls
- Mixing command I/O with domain logic in `handle(...)`.
- Missing/ambiguous command signature.
## References
- https://www.laravelactions.com/2.x/as-command.html

View file

@ -0,0 +1,339 @@
# Controller Entrypoint (`asController`)
## Scope
Use this reference when exposing an action through HTTP routes.
## Recap
- Documents controller lifecycle around `asController(...)` and response adapters.
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
- Summarizes validation/authorization hooks used by `ActionRequest`.
- Provides extension points for JSON/HTML responses and failure customization.
## Recommended pattern
- Route directly to action class when appropriate.
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
- Keep domain logic in `handle(...)`.
## Methods provided (`AsController` trait)
### `__invoke`
Required so Laravel can register the action class as an invokable controller.
```php
$action($someArguments);
// Equivalent to:
$action->handle($someArguments);
```
If the method does not exist, Laravel route registration fails for invokable controllers.
```php
// Illuminate\Routing\RouteAction
protected static function makeInvokable($action)
{
if (! method_exists($action, '__invoke')) {
throw new UnexpectedValueException("Invalid route action: [{$action}].");
}
return $action.'@__invoke';
}
```
If you need your own `__invoke`, alias the trait implementation:
```php
class MyAction
{
use AsAction {
__invoke as protected invokeFromLaravelActions;
}
public function __invoke()
{
// Custom behavior...
}
}
```
## Methods used (`ControllerDecorator` + `ActionRequest`)
### `asController`
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
```php
public function asController(User $user, Request $request): Response
{
$article = $this->handle(
$user,
$request->get('title'),
$request->get('body')
);
return redirect()->route('articles.show', [$article]);
}
```
### `jsonResponse`
Called after `asController` when request expects JSON.
```php
public function jsonResponse(Article $article, Request $request): ArticleResource
{
return new ArticleResource($article);
}
```
### `htmlResponse`
Called after `asController` when request expects HTML.
```php
public function htmlResponse(Article $article, Request $request): Response
{
return redirect()->route('articles.show', [$article]);
}
```
### `getControllerMiddleware`
Adds middleware directly on the action controller.
```php
public function getControllerMiddleware(): array
{
return ['auth', MyCustomMiddleware::class];
}
```
### `routes`
Defines routes directly in the action.
```php
public static function routes(Router $router)
{
$router->get('author/{author}/articles', static::class);
}
```
To enable this, register routes from actions in a service provider:
```php
use Lorisleiva\Actions\Facades\Actions;
Actions::registerRoutes();
Actions::registerRoutes('app/MyCustomActionsFolder');
Actions::registerRoutes([
'app/Authentication',
'app/Billing',
'app/TeamManagement',
]);
```
### `prepareForValidation`
Called before authorization and validation are resolved.
```php
public function prepareForValidation(ActionRequest $request): void
{
$request->merge(['some' => 'additional data']);
}
```
### `authorize`
Defines authorization logic.
```php
public function authorize(ActionRequest $request): bool
{
return $request->user()->role === 'author';
}
```
You can also return gate responses:
```php
use Illuminate\Auth\Access\Response;
public function authorize(ActionRequest $request): Response
{
if ($request->user()->role !== 'author') {
return Response::deny('You must be an author to create a new article.');
}
return Response::allow();
}
```
### `rules`
Defines validation rules.
```php
public function rules(): array
{
return [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
];
}
```
### `withValidator`
Adds custom validation logic with an after hook.
```php
use Illuminate\Validation\Validator;
public function withValidator(Validator $validator, ActionRequest $request): void
{
$validator->after(function (Validator $validator) use ($request) {
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
});
}
```
### `afterValidator`
Alternative to add post-validation checks.
```php
use Illuminate\Validation\Validator;
public function afterValidator(Validator $validator, ActionRequest $request): void
{
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
}
```
### `getValidator`
Provides a custom validator instead of default rules pipeline.
```php
use Illuminate\Validation\Factory;
use Illuminate\Validation\Validator;
public function getValidator(Factory $factory, ActionRequest $request): Validator
{
return $factory->make($request->only('title', 'body'), [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
]);
}
```
### `getValidationData`
Defines which data is validated (default: `$request->all()`).
```php
public function getValidationData(ActionRequest $request): array
{
return $request->all();
}
```
### `getValidationMessages`
Custom validation error messages.
```php
public function getValidationMessages(): array
{
return [
'title.required' => 'Looks like you forgot the title.',
'body.required' => 'Is that really all you have to say?',
];
}
```
### `getValidationAttributes`
Human-friendly names for request attributes.
```php
public function getValidationAttributes(): array
{
return [
'title' => 'headline',
'body' => 'content',
];
}
```
### `getValidationRedirect`
Custom redirect URL on validation failure.
```php
public function getValidationRedirect(UrlGenerator $url): string
{
return $url->to('/my-custom-redirect-url');
}
```
### `getValidationErrorBag`
Custom error bag name on validation failure (default: `default`).
```php
public function getValidationErrorBag(): string
{
return 'my_custom_error_bag';
}
```
### `getValidationFailure`
Override validation failure behavior.
```php
public function getValidationFailure(): void
{
throw new MyCustomValidationException();
}
```
### `getAuthorizationFailure`
Override authorization failure behavior.
```php
public function getAuthorizationFailure(): void
{
throw new MyCustomAuthorizationException();
}
```
## Checklist
- Route wiring points to the action class.
- `asController(...)` delegates to `handle(...)`.
- Validation/authorization methods are explicit where needed.
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
- HTTP tests cover both success and validation/authorization failure branches.
## Common pitfalls
- Putting response/redirect logic in `handle(...)`.
- Duplicating business rules in `asController(...)` instead of delegating.
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
## References
- https://www.laravelactions.com/2.x/as-controller.html

View file

@ -0,0 +1,425 @@
# Job Entrypoint (`dispatch`, `asJob`)
## Scope
Use this reference when running an action through queues.
## Recap
- Lists async/sync dispatch helpers and conditional dispatch variants.
- Covers job wrapping/chaining with `makeJob`, `makeUniqueJob`, and `withChain`.
- Documents queue assertion helpers for tests (`assertPushed*`).
- Summarizes `JobDecorator` hooks/properties for retries, uniqueness, timeout, and failure handling.
## Recommended pattern
- Dispatch with `Action::dispatch(...)` for async execution.
- Keep queue-specific orchestration in `asJob(...)`.
- Keep reusable business logic in `handle(...)`.
## Methods provided (`AsJob` trait)
### `dispatch`
Dispatches the action asynchronously.
```php
SendTeamReportEmail::dispatch($team);
```
### `dispatchIf`
Dispatches asynchronously only if condition is met.
```php
SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team);
```
### `dispatchUnless`
Dispatches asynchronously unless condition is met.
```php
SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);
```
### `dispatchSync`
Dispatches synchronously.
```php
SendTeamReportEmail::dispatchSync($team);
```
### `dispatchNow`
Alias of `dispatchSync`.
```php
SendTeamReportEmail::dispatchNow($team);
```
### `dispatchAfterResponse`
Dispatches synchronously after the HTTP response is sent.
```php
SendTeamReportEmail::dispatchAfterResponse($team);
```
### `makeJob`
Creates a `JobDecorator` wrapper. Useful with `dispatch(...)` helper or chains.
```php
dispatch(SendTeamReportEmail::makeJob($team));
```
### `makeUniqueJob`
Creates a `UniqueJobDecorator` wrapper. Usually automatic with `ShouldBeUnique`, but can be forced.
```php
dispatch(SendTeamReportEmail::makeUniqueJob($team));
```
### `withChain`
Attaches jobs to run after successful processing.
```php
$chain = [
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
];
CreateNewTeamReport::withChain($chain)->dispatch($team);
```
Equivalent using `Bus::chain(...)`:
```php
use Illuminate\Support\Facades\Bus;
Bus::chain([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
])->dispatch();
```
Chain assertion example:
```php
use Illuminate\Support\Facades\Bus;
Bus::fake();
Bus::assertChained([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
]);
```
### `assertPushed`
Asserts the action was queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushed();
SendTeamReportEmail::assertPushed(3);
SendTeamReportEmail::assertPushed($callback);
SendTeamReportEmail::assertPushed(3, $callback);
```
`$callback` receives:
- Action instance.
- Dispatched arguments.
- `JobDecorator` instance.
- Queue name.
### `assertNotPushed`
Asserts the action was not queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertNotPushed();
SendTeamReportEmail::assertNotPushed($callback);
```
### `assertPushedOn`
Asserts the action was queued on a specific queue.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushedOn('reports');
SendTeamReportEmail::assertPushedOn('reports', 3);
SendTeamReportEmail::assertPushedOn('reports', $callback);
SendTeamReportEmail::assertPushedOn('reports', 3, $callback);
```
## Methods used (`JobDecorator`)
### `asJob`
Called when dispatched as a job. Falls back to `handle(...)` if missing.
```php
class SendTeamReportEmail
{
use AsAction;
public function handle(Team $team, bool $fullReport = false): void
{
// Prepare report and send it to all $team->users.
}
public function asJob(Team $team): void
{
$this->handle($team, true);
}
}
```
### `getJobMiddleware`
Adds middleware to the queued action.
```php
public function getJobMiddleware(array $parameters): array
{
return [new RateLimited('reports')];
}
```
### `configureJob`
Configures `JobDecorator` options.
```php
use Lorisleiva\Actions\Decorators\JobDecorator;
public function configureJob(JobDecorator $job): void
{
$job->onConnection('my_connection')
->onQueue('my_queue')
->through(['my_middleware'])
->chain(['my_chain'])
->delay(60);
}
```
### `$jobConnection`
Defines queue connection.
```php
public string $jobConnection = 'my_connection';
```
### `$jobQueue`
Defines queue name.
```php
public string $jobQueue = 'my_queue';
```
### `$jobTries`
Defines max attempts.
```php
public int $jobTries = 10;
```
### `$jobMaxExceptions`
Defines max unhandled exceptions before failure.
```php
public int $jobMaxExceptions = 3;
```
### `$jobBackoff`
Defines retry delay seconds.
```php
public int $jobBackoff = 60;
```
### `getJobBackoff`
Defines retry delay (int or per-attempt array).
```php
public function getJobBackoff(): int
{
return 60;
}
public function getJobBackoff(): array
{
return [30, 60, 120];
}
```
### `$jobTimeout`
Defines timeout in seconds.
```php
public int $jobTimeout = 60 * 30;
```
### `$jobRetryUntil`
Defines timestamp retry deadline.
```php
public int $jobRetryUntil = 1610191764;
```
### `getJobRetryUntil`
Defines retry deadline as `DateTime`.
```php
public function getJobRetryUntil(): DateTime
{
return now()->addMinutes(30);
}
```
### `getJobDisplayName`
Customizes queued job display name.
```php
public function getJobDisplayName(): string
{
return 'Send team report email';
}
```
### `getJobTags`
Adds queue tags.
```php
public function getJobTags(Team $team): array
{
return ['report', 'team:'.$team->id];
}
```
### `getJobUniqueId`
Defines uniqueness key when using `ShouldBeUnique`.
```php
public function getJobUniqueId(Team $team): int
{
return $team->id;
}
```
### `$jobUniqueId`
Static uniqueness key alternative.
```php
public string $jobUniqueId = 'some_static_key';
```
### `getJobUniqueFor`
Defines uniqueness lock duration in seconds.
```php
public function getJobUniqueFor(Team $team): int
{
return $team->role === 'premium' ? 1800 : 3600;
}
```
### `$jobUniqueFor`
Property alternative for uniqueness lock duration.
```php
public int $jobUniqueFor = 3600;
```
### `getJobUniqueVia`
Defines cache driver used for uniqueness lock.
```php
public function getJobUniqueVia()
{
return Cache::driver('redis');
}
```
### `$jobDeleteWhenMissingModels`
Property alternative for missing model handling.
```php
public bool $jobDeleteWhenMissingModels = true;
```
### `getJobDeleteWhenMissingModels`
Defines whether jobs with missing models are deleted.
```php
public function getJobDeleteWhenMissingModels(): bool
{
return true;
}
```
### `jobFailed`
Handles job failure. Receives exception and dispatched parameters.
```php
public function jobFailed(?Throwable $e, ...$parameters): void
{
// Notify users, report errors, trigger compensations...
}
```
## Checklist
- Async/sync dispatch method matches use-case (`dispatch`, `dispatchSync`, `dispatchAfterResponse`).
- Queue config is explicit when needed (`$jobConnection`, `$jobQueue`, `configureJob`).
- Retry/backoff/timeout policies are intentional.
- `asJob(...)` delegates to `handle(...)` unless queue-specific branching is required.
- Queue tests use `Queue::fake()` and action assertions (`assertPushed*`).
## Common pitfalls
- Embedding domain logic only in `asJob(...)`.
- Forgetting uniqueness/timeout/retry controls on heavy jobs.
- Missing queue-specific assertions in tests.
## References
- https://www.laravelactions.com/2.x/as-job.html

View file

@ -0,0 +1,81 @@
# Listener Entrypoint (`asListener`)
## Scope
Use this reference when wiring actions to domain/application events.
## Recap
- Shows how listener execution maps event payloads into `handle(...)` arguments.
- Describes `asListener(...)` fallback behavior and adaptation role.
- Includes event registration example for provider wiring.
- Emphasizes test focus on dispatch and action interaction.
## Recommended pattern
- Register action listener in `EventServiceProvider` (or project equivalent).
- Use `asListener(Event $event)` for event adaptation.
- Delegate core logic to `handle(...)`.
## Methods used (`ListenerDecorator`)
### `asListener`
Called when executed as an event listener. If missing, it falls back to `handle(...)`.
```php
class SendOfferToNearbyDrivers
{
use AsAction;
public function handle(Address $source, Address $destination): void
{
// ...
}
public function asListener(TaxiRequested $event): void
{
$this->handle($event->source, $event->destination);
}
}
```
## Examples
### Event registration
```php
// app/Providers/EventServiceProvider.php
protected $listen = [
TaxiRequested::class => [
SendOfferToNearbyDrivers::class,
],
];
```
### Focused listener test
```php
use Illuminate\Support\Facades\Event;
Event::fake();
TaxiRequested::dispatch($source, $destination);
Event::assertDispatched(TaxiRequested::class);
```
## Checklist
- Event-to-listener mapping is registered.
- Listener method signature matches event contract.
- Listener tests verify dispatch and action interaction.
## Common pitfalls
- Assuming automatic listener registration when explicit mapping is required.
- Re-implementing business logic in `asListener(...)`.
## References
- https://www.laravelactions.com/2.x/as-listener.html

View file

@ -0,0 +1,118 @@
# Object Entrypoint (`run`, `make`, DI)
## Scope
Use this reference when the action is invoked as a plain object.
## Recap
- Explains object-style invocation with `make`, `run`, `runIf`, `runUnless`.
- Clarifies when to use static helpers versus DI/manual invocation.
- Includes minimal examples for direct run and service-level injection.
- Highlights boundaries: business logic stays in `handle(...)`.
## Recommended pattern
- Keep core business logic in `handle(...)`.
- Prefer `Action::run(...)` for readability.
- Use `Action::make()->handle(...)` or DI only when needed.
## Methods provided
### `make`
Resolves the action from the container.
```php
PublishArticle::make();
// Equivalent to:
app(PublishArticle::class);
```
### `run`
Resolves and executes the action.
```php
PublishArticle::run($articleId);
// Equivalent to:
PublishArticle::make()->handle($articleId);
```
### `runIf`
Resolves and executes the action only if the condition is met.
```php
PublishArticle::runIf($shouldPublish, $articleId);
// Equivalent mental model:
if ($shouldPublish) {
PublishArticle::run($articleId);
}
```
### `runUnless`
Resolves and executes the action only if the condition is not met.
```php
PublishArticle::runUnless($alreadyPublished, $articleId);
// Equivalent mental model:
if (! $alreadyPublished) {
PublishArticle::run($articleId);
}
```
## Checklist
- Input/output types are explicit.
- `handle(...)` has no transport concerns.
- Business behavior is covered by direct `handle(...)` tests.
## Common pitfalls
- Putting HTTP/CLI/queue concerns in `handle(...)`.
- Calling adapters from `handle(...)` instead of the reverse.
## References
- https://www.laravelactions.com/2.x/as-object.html
## Examples
### Minimal object-style invocation
```php
final class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
// Domain logic...
return true;
}
}
$published = PublishArticle::run(42);
```
### Dependency injection invocation
```php
final class ArticleService
{
public function __construct(
private PublishArticle $publishArticle
) {}
public function publish(int $articleId): bool
{
return $this->publishArticle->handle($articleId);
}
}
```

View file

@ -0,0 +1,160 @@
# Testing and Action Fakes
## Scope
Use this reference when isolating action orchestration in tests.
## Recap
- Summarizes all `AsFake` helpers (`mock`, `partialMock`, `spy`, `shouldRun`, `shouldNotRun`, `allowToRun`).
- Clarifies when to assert execution versus non-execution.
- Covers fake lifecycle checks/reset (`isFake`, `clearFake`).
- Provides branch-oriented test examples for orchestration confidence.
## Core methods
- `mock()`
- `partialMock()`
- `spy()`
- `shouldRun()`
- `shouldNotRun()`
- `allowToRun()`
- `isFake()`
- `clearFake()`
## Recommended pattern
- Test `handle(...)` directly for business rules.
- Test entrypoints for wiring/orchestration.
- Fake only at the boundary under test.
## Methods provided (`AsFake` trait)
### `mock`
Swaps the action with a full mock.
```php
FetchContactsFromGoogle::mock()
->shouldReceive('handle')
->with(42)
->andReturn(['Loris', 'Will', 'Barney']);
```
### `partialMock`
Swaps the action with a partial mock.
```php
FetchContactsFromGoogle::partialMock()
->shouldReceive('fetch')
->with('some_google_identifier')
->andReturn(['Loris', 'Will', 'Barney']);
```
### `spy`
Swaps the action with a spy.
```php
$spy = FetchContactsFromGoogle::spy()
->allows('handle')
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `shouldRun`
Helper adding expectation on `handle`.
```php
FetchContactsFromGoogle::shouldRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldReceive('handle');
```
### `shouldNotRun`
Helper adding negative expectation on `handle`.
```php
FetchContactsFromGoogle::shouldNotRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldNotReceive('handle');
```
### `allowToRun`
Helper allowing `handle` on a spy.
```php
$spy = FetchContactsFromGoogle::allowToRun()
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `isFake`
Returns whether the action has been swapped with a fake.
```php
FetchContactsFromGoogle::isFake(); // false
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
```
### `clearFake`
Clears the fake instance, if any.
```php
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
FetchContactsFromGoogle::clearFake();
FetchContactsFromGoogle::isFake(); // false
```
## Examples
### Orchestration test
```php
it('runs sync contacts for premium teams', function () {
SyncGoogleContacts::shouldRun()->once()->with(42)->andReturnTrue();
ImportTeamContacts::run(42, isPremium: true);
});
```
### Guard-clause test
```php
it('does not run sync when integration is disabled', function () {
SyncGoogleContacts::shouldNotRun();
ImportTeamContacts::run(42, integrationEnabled: false);
});
```
## Checklist
- Assertions verify call intent and argument contracts.
- Fakes are cleared when leakage risk exists.
- Branch tests use `shouldRun()` / `shouldNotRun()` where clearer.
## Common pitfalls
- Over-mocking and losing behavior confidence.
- Asserting only dispatch, not business correctness.
## References
- https://www.laravelactions.com/2.x/as-fake.html

View file

@ -0,0 +1,33 @@
# Troubleshooting
## Scope
Use this reference when action wiring behaves unexpectedly.
## Recap
- Provides a fast triage flow for routing, queueing, events, and command wiring.
- Lists recurring failure patterns and where to check first.
- Encourages reproducing issues with focused tests before broad debugging.
- Separates wiring diagnostics from domain logic verification.
## Fast checks
- Action class uses `AsAction`.
- Namespace and autoloading are correct.
- Entrypoint wiring (route, queue, event, command) is registered.
- Method signatures and argument types match caller expectations.
## Failure patterns
- Controller route points to wrong class.
- Queue worker/config mismatch.
- Listener mapping not loaded.
- Command signature mismatch.
- Command not registered in the console kernel.
## Debug checklist
- Reproduce with a focused failing test.
- Validate wiring layer first, then domain behavior.
- Isolate dependencies with fakes/spies where appropriate.

View file

@ -0,0 +1,189 @@
# With Attributes (`WithAttributes` trait)
## Scope
Use this reference when an action stores and validates input via internal attributes instead of method arguments.
## Recap
- Documents attribute lifecycle APIs (`setRawAttributes`, `fill`, `fillFromRequest`, readers/writers).
- Clarifies behavior of key collisions (`fillFromRequest`: request data wins over route params).
- Lists validation/authorization hooks reused from controller validation pipeline.
- Includes end-to-end example from fill to `validateAttributes()` and `handle(...)`.
## Methods provided (`WithAttributes` trait)
### `setRawAttributes`
Replaces all attributes with the provided payload.
```php
$action->setRawAttributes([
'key' => 'value',
]);
```
### `fill`
Merges provided attributes into existing attributes.
```php
$action->fill([
'key' => 'value',
]);
```
### `fillFromRequest`
Merges request input and route parameters into attributes. Request input has priority over route parameters when keys collide.
```php
$action->fillFromRequest($request);
```
### `all`
Returns all attributes.
```php
$action->all();
```
### `only`
Returns attributes matching the provided keys.
```php
$action->only('title', 'body');
```
### `except`
Returns attributes excluding the provided keys.
```php
$action->except('body');
```
### `has`
Returns whether an attribute exists for the given key.
```php
$action->has('title');
```
### `get`
Returns the attribute value by key, with optional default.
```php
$action->get('title');
$action->get('title', 'Untitled');
```
### `set`
Sets an attribute value by key.
```php
$action->set('title', 'My blog post');
```
### `__get`
Accesses attributes as object properties.
```php
$action->title;
```
### `__set`
Updates attributes as object properties.
```php
$action->title = 'My blog post';
```
### `__isset`
Checks attribute existence as object properties.
```php
isset($action->title);
```
### `validateAttributes`
Runs authorization and validation using action attributes and returns validated data.
```php
$validatedData = $action->validateAttributes();
```
## Methods used (`AttributeValidator`)
`WithAttributes` uses the same authorization/validation hooks as `AsController`:
- `prepareForValidation`
- `authorize`
- `rules`
- `withValidator`
- `afterValidator`
- `getValidator`
- `getValidationData`
- `getValidationMessages`
- `getValidationAttributes`
- `getValidationRedirect`
- `getValidationErrorBag`
- `getValidationFailure`
- `getAuthorizationFailure`
## Example
```php
class CreateArticle
{
use AsAction;
use WithAttributes;
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:8'],
'body' => ['required', 'string'],
];
}
public function handle(array $attributes): Article
{
return Article::create($attributes);
}
}
$action = CreateArticle::make()->fill([
'title' => 'My first post',
'body' => 'Hello world',
]);
$validated = $action->validateAttributes();
$article = $action->handle($validated);
```
## Checklist
- Attribute keys are explicit and stable.
- Validation rules match expected attribute shape.
- `validateAttributes()` is called before side effects when needed.
- Validation/authorization hooks are tested in focused unit tests.
## Common pitfalls
- Mixing attribute-based and argument-based flows inconsistently in the same action.
- Assuming route params override request input in `fillFromRequest` (they do not).
- Skipping `validateAttributes()` when using external input.
## References
- https://www.laravelactions.com/2.x/with-attributes.html

View file

@ -0,0 +1,190 @@
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
author: laravel
---
# Laravel Best Practices
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
## Consistency First
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
## Quick Reference
### 1. Database Performance → `rules/db-performance.md`
- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
### 3. Security → `rules/security.md`
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
### 4. Caching → `rules/caching.md`
- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production
### 5. Eloquent Patterns → `rules/eloquent.md`
- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
### 6. Validation & Forms → `rules/validation.md`
- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`
### 7. Configuration → `rules/config.md`
- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text
### 8. Testing Patterns → `rules/testing.md`
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios
### 10. Routing & Controllers → `rules/routing.md`
- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation
### 11. HTTP Client → `rules/http-client.md`
- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails
### 13. Error Handling → `rules/error-handling.md`
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes
### 14. Task Scheduling → `rules/scheduling.md`
- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration
### 15. Architecture → `rules/architecture.md`
- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
### 16. Migrations → `rules/migrations.md`
- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML
### 17. Collections → `rules/collections.md`
- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections
### 18. Blade & Views → `rules/blade-views.md`
- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props
### 19. Conventions & Style → `rules/style.md`
- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files
## How to Apply
Always use a sub-agent to read rule files and explore this skill's content.
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version

View file

@ -0,0 +1,106 @@
# Advanced Query Patterns
## Use `addSelect()` Subqueries for Single Values from Has-Many
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
```php
public function scopeWithLastLoginAt($query): void
{
$query->addSelect([
'last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->withCasts(['last_login_at' => 'datetime']);
}
```
## Create Dynamic Relationships via Subquery FK
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
```php
public function lastLogin(): BelongsTo
{
return $this->belongsTo(Login::class);
}
public function scopeWithLastLogin($query): void
{
$query->addSelect([
'last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}
```
## Use Conditional Aggregates Instead of Multiple Count Queries
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
```php
$statuses = Feature::toBase()
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
->first();
```
## Use `setRelation()` to Prevent Circular N+1
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
```php
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
```
## Prefer `whereIn` + Subquery Over `whereHas`
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
Incorrect (correlated EXISTS re-executes per row):
```php
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
```
Correct (index-friendly subquery, no PHP memory overhead):
```php
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
```
## Sometimes Two Simple Queries Beat One Complex Query
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
## Use Compound Indexes Matching `orderBy` Column Order
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
```php
// Migration
$table->index(['last_name', 'first_name']);
// Query — column order must match the index
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
```
## Use Correlated Subqueries for Has-Many Ordering
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
```php
public function scopeOrderByLastLogin($query): void
{
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
```

View file

@ -0,0 +1,202 @@
# Architecture Best Practices
## Single-Purpose Action Classes
Extract discrete business operations into invokable Action classes.
```php
class CreateOrderAction
{
public function __construct(private InventoryService $inventory) {}
public function execute(array $data): Order
{
$order = Order::create($data);
$this->inventory->reserve($order);
return $order;
}
}
```
## Use Dependency Injection
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
Incorrect:
```php
class OrderController extends Controller
{
public function store(StoreOrderRequest $request)
{
$service = app(OrderService::class);
return $service->create($request->validated());
}
}
```
Correct:
```php
class OrderController extends Controller
{
public function __construct(private OrderService $service) {}
public function store(StoreOrderRequest $request)
{
return $this->service->create($request->validated());
}
}
```
## Code to Interfaces
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
Incorrect (concrete dependency):
```php
class OrderService
{
public function __construct(private StripeGateway $gateway) {}
}
```
Correct (interface dependency):
```php
interface PaymentGateway
{
public function charge(int $amount, string $customerId): PaymentResult;
}
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
}
```
Bind in a service provider:
```php
$this->app->bind(PaymentGateway::class, StripeGateway::class);
```
## Default Sort by Descending
When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres.
Incorrect:
```php
$posts = Post::paginate();
```
Correct:
```php
$posts = Post::latest()->paginate();
```
## Use Atomic Locks for Race Conditions
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
```php
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
$order->process();
});
// Or at query level
$product = Product::where('id', $id)->lockForUpdate()->first();
```
## Use `mb_*` String Functions
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
Incorrect:
```php
strlen('José'); // 5 (bytes, not characters)
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
```
Correct:
```php
mb_strlen('José'); // 4 (characters)
mb_strtolower('MÜNCHEN'); // 'münchen'
// Prefer Laravel's Str helpers when available
Str::length('José'); // 4
Str::lower('MÜNCHEN'); // 'münchen'
```
## Use `defer()` for Post-Response Work
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
Incorrect (job overhead for trivial work):
```php
dispatch(new LogPageView($page));
```
Correct (runs after response, same process):
```php
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
```
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
## Use `Context` for Request-Scoped Data
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
```php
// In middleware
Context::add('tenant_id', $request->header('X-Tenant-ID'));
// Anywhere later — controllers, jobs, log context
$tenantId = Context::get('tenant_id');
```
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
## Use `Concurrency::run()` for Parallel Execution
Run independent operations in parallel using child processes — no async libraries needed.
```php
use Illuminate\Support\Facades\Concurrency;
[$users, $orders] = Concurrency::run([
fn () => User::count(),
fn () => Order::where('status', 'pending')->count(),
]);
```
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
## Convention Over Configuration
Follow Laravel conventions. Don't override defaults unnecessarily.
Incorrect:
```php
class Customer extends Model
{
protected $table = 'Customer';
protected $primaryKey = 'customer_id';
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
}
}
```
Correct:
```php
class Customer extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
```

View file

@ -0,0 +1,36 @@
# Blade & Views Best Practices
## Use `$attributes->merge()` in Component Templates
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
```blade
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
```
## Use `@pushOnce` for Per-Component Scripts
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
## Prefer Blade Components Over `@include`
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
## Use View Composers for Shared View Data
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
A single view can return either the full page or just a fragment, keeping routing clean.
```php
return view('dashboard', compact('users'))
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
```
## Use `@aware` for Deeply Nested Component Props
Avoids re-passing parent props through every level of nested components.

View file

@ -0,0 +1,70 @@
# Caching Best Practices
## Use `Cache::remember()` Instead of Manual Get/Put
Atomic pattern prevents race conditions and removes boilerplate.
Incorrect:
```php
$val = Cache::get('stats');
if (! $val) {
$val = $this->computeStats();
Cache::put('stats', $val, 60);
}
```
Correct:
```php
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
```
## Use `Cache::flexible()` for Stale-While-Revalidate
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
## Use Cache Tags to Invalidate Related Groups
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
```php
Cache::tags(['user-1'])->flush();
```
## Use `Cache::add()` for Atomic Conditional Writes
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
Correct: `Cache::add('lock', true, 10);`
## Use `once()` for Per-Request Memoization
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
```php
public function roles(): Collection
{
return once(fn () => $this->loadRoles());
}
```
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
## Configure Failover Cache Stores in Production
If Redis goes down, the app falls back to a secondary store automatically.
```php
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
```

View file

@ -0,0 +1,44 @@
# Collection Best Practices
## Use Higher-Order Messages for Simple Operations
Incorrect:
```php
$users->each(function (User $user) {
$user->markAsVip();
});
```
Correct: `$users->each->markAsVip();`
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
## Choose `cursor()` vs. `lazy()` Correctly
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
## Use `lazyById()` When Updating Records While Iterating
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
## Use `toQuery()` for Bulk Operations on Collections
Avoids manual `whereIn` construction.
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
Correct: `$users->toQuery()->update([...]);`
## Use `#[CollectedBy]` for Custom Collection Classes
More declarative than overriding `newCollection()`.
```php
#[CollectedBy(UserCollection::class)]
class User extends Model {}
```

View file

@ -0,0 +1,73 @@
# Configuration Best Practices
## `env()` Only in Config Files
Direct `env()` calls return `null` when config is cached.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'key' => env('API_KEY'),
// Application code
$key = config('services.key');
```
## Use Encrypted Env or External Secrets
Never store production secrets in plain `.env` files in version control.
Incorrect:
```bash
# .env committed to repo or shared in Slack
STRIPE_SECRET=sk_live_abc123
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
```
Correct:
```bash
php artisan env:encrypt --env=production --readable
php artisan env:decrypt --env=production
```
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
## Use `App::environment()` for Environment Checks
Incorrect:
```php
if (env('APP_ENV') === 'production') {
```
Correct:
```php
if (app()->isProduction()) {
// or
if (App::environment('production')) {
```
## Use Constants and Language Files
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
```php
// Incorrect
return $this->type === 'normal';
// Correct
return $this->type === self::TYPE_NORMAL;
```
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
```php
// Only when lang files already exist in the project
return back()->with('message', __('app.article_added'));
```

View file

@ -0,0 +1,192 @@
# Database Performance Best Practices
## Always Eager Load Relationships
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
Incorrect (N+1 — executes 1 + N queries):
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Correct (2 queries total):
```php
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
```
Constrain eager loads to select only needed columns (always include the foreign key):
```php
$users = User::with(['posts' => function ($query) {
$query->select('id', 'user_id', 'title')
->where('published', true)
->latest()
->limit(10);
}])->get();
```
## Prevent Lazy Loading in Development
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
```php
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
```
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
## Select Only Needed Columns
Avoid `SELECT *` — especially when tables have large text or JSON columns.
Incorrect:
```php
$posts = Post::with('author')->get();
```
Correct:
```php
$posts = Post::select('id', 'title', 'user_id', 'created_at')
->with(['author:id,name,avatar'])
->get();
```
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
## Chunk Large Datasets
Never load thousands of records at once. Use chunking for batch processing.
Incorrect:
```php
$users = User::all();
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
```
Correct:
```php
User::where('subscribed', true)->chunk(200, function ($users) {
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
});
```
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
```php
User::where('active', false)->chunkById(200, function ($users) {
$users->each->delete();
});
```
## Add Database Indexes
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index()->constrained();
$table->string('status')->index();
$table->timestamps();
$table->index(['status', 'created_at']);
});
```
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
## Use `withCount()` for Counting Relations
Never load entire collections just to count them.
Incorrect:
```php
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
```
Correct:
```php
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
```
Conditional counting:
```php
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => function ($query) {
$query->where('approved', true);
},
])->get();
```
## Use `cursor()` for Memory-Efficient Iteration
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
Incorrect:
```php
$users = User::where('active', true)->get();
```
Correct:
```php
foreach (User::where('active', true)->cursor() as $user) {
ProcessUser::dispatch($user->id);
}
```
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
## No Queries in Blade Templates
Never execute queries in Blade templates. Pass data from controllers.
Incorrect:
```blade
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
```
Correct:
```php
// Controller
$users = User::with('profile')->get();
return view('users.index', compact('users'));
```
```blade
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
```

View file

@ -0,0 +1,148 @@
# Eloquent Best Practices
## Use Correct Relationship Types
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
```php
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
```
## Use Local Scopes for Reusable Queries
Extract reusable query constraints into local scopes to avoid duplication.
Incorrect:
```php
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
$articles = Article::whereHas('user', function ($q) {
$q->where('verified', true)->whereNotNull('activated_at');
})->get();
```
Correct:
```php
public function scopeActive(Builder $query): Builder
{
return $query->where('verified', true)->whereNotNull('activated_at');
}
// Usage
$active = User::active()->get();
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
```
## Apply Global Scopes Sparingly
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
Incorrect (global scope for a conditional filter):
```php
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('published', true);
}
}
// Now admin panels, reports, and background jobs all silently skip drafts
```
Correct (local scope you opt into):
```php
public function scopePublished(Builder $query): Builder
{
return $query->where('published', true);
}
Post::published()->paginate(); // Explicit
Post::paginate(); // Admin sees all
```
## Define Attribute Casts
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
```php
protected function casts(): array
{
return [
'is_active' => 'boolean',
'metadata' => 'array',
'total' => 'decimal:2',
];
}
```
## Cast Date Columns Properly
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
Incorrect:
```blade
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
```
Correct:
```php
protected function casts(): array
{
return [
'ordered_at' => 'datetime',
];
}
```
```blade
{{ $order->ordered_at->toDateString() }}
{{ $order->ordered_at->format('m-d') }}
```
## Use `whereBelongsTo()` for Relationship Queries
Cleaner than manually specifying foreign keys.
Incorrect:
```php
Post::where('user_id', $user->id)->get();
```
Correct:
```php
Post::whereBelongsTo($user)->get();
Post::whereBelongsTo($user, 'author')->get();
```
## Avoid Hardcoded Table Names in Queries
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
Incorrect:
```php
DB::table('users')->where('active', true)->get();
$query->join('companies', 'companies.id', '=', 'users.company_id');
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
```
Correct — reference the model's table:
```php
DB::table((new User)->getTable())->where('active', true)->get();
// Even better — use Eloquent or the query builder instead of raw SQL
User::where('active', true)->get();
Order::where('status', 'pending')->get();
```
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.

View file

@ -0,0 +1,72 @@
# Error Handling Best Practices
## Exception Reporting and Rendering
There are two valid approaches — choose one and apply it consistently across the project.
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
```php
class InvalidOrderException extends Exception
{
public function report(): void { /* custom reporting */ }
public function render(Request $request): Response
{
return response()->view('errors.invalid-order', status: 422);
}
}
```
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
```php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
$exceptions->render(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', status: 422);
});
})
```
Check the existing codebase and follow whichever pattern is already established.
## Use `ShouldntReport` for Exceptions That Should Never Log
More discoverable than listing classes in `dontReport()`.
```php
class PodcastProcessingException extends Exception implements ShouldntReport {}
```
## Throttle High-Volume Exceptions
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
## Enable `dontReportDuplicates()`
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
## Force JSON Error Rendering for API Routes
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
```php
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
return $request->is('api/*') || $request->expectsJson();
});
```
## Add Context to Exception Classes
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
```php
class InvalidOrderException extends Exception
{
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
```

View file

@ -0,0 +1,48 @@
# Events & Notifications Best Practices
## Rely on Event Discovery
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
## Run `event:cache` in Production Deploy
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
## Use `ShouldDispatchAfterCommit` Inside Transactions
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
```php
class OrderShipped implements ShouldDispatchAfterCommit {}
```
## Always Queue Notifications
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
```php
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
}
```
## Use `afterCommit()` on Notifications in Transactions
Same race condition as events — the queued notification job may run before the transaction commits.
## Route Notification Channels to Dedicated Queues
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
## Use On-Demand Notifications for Non-User Recipients
Avoid creating dummy models to send notifications to arbitrary addresses.
```php
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
```
## Implement `HasLocalePreference` on Notifiable Models
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.

View file

@ -0,0 +1,160 @@
# HTTP Client Best Practices
## Always Set Explicit Timeouts
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
Incorrect:
```php
$response = Http::get('https://api.example.com/users');
```
Correct:
```php
$response = Http::timeout(5)
->connectTimeout(3)
->get('https://api.example.com/users');
```
For service-specific clients, define timeouts in a macro:
```php
Http::macro('github', function () {
return Http::baseUrl('https://api.github.com')
->timeout(10)
->connectTimeout(3)
->withToken(config('services.github.token'));
});
$response = Http::github()->get('/repos/laravel/framework');
```
## Use Retry with Backoff for External APIs
External APIs have transient failures. Use `retry()` with increasing delays.
Incorrect:
```php
$response = Http::post('https://api.stripe.com/v1/charges', $data);
if ($response->failed()) {
throw new PaymentFailedException('Charge failed');
}
```
Correct:
```php
$response = Http::retry([100, 500, 1000])
->timeout(10)
->post('https://api.stripe.com/v1/charges', $data);
```
Only retry on specific errors:
```php
$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {
return $exception instanceof ConnectionException
|| ($exception instanceof RequestException && $exception->response->serverError());
})->post('https://api.example.com/data');
```
## Handle Errors Explicitly
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
Incorrect:
```php
$response = Http::get('https://api.example.com/users/1');
$user = $response->json(); // Could be an error body
```
Correct:
```php
$response = Http::timeout(5)
->get('https://api.example.com/users/1')
->throw();
$user = $response->json();
```
For graceful degradation:
```php
$response = Http::get('https://api.example.com/users/1');
if ($response->successful()) {
return $response->json();
}
if ($response->notFound()) {
return null;
}
$response->throw();
```
## Use Request Pooling for Concurrent Requests
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
Incorrect:
```php
$users = Http::get('https://api.example.com/users')->json();
$posts = Http::get('https://api.example.com/posts')->json();
$comments = Http::get('https://api.example.com/comments')->json();
```
Correct:
```php
use Illuminate\Http\Client\Pool;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('users')->get('https://api.example.com/users'),
$pool->as('posts')->get('https://api.example.com/posts'),
$pool->as('comments')->get('https://api.example.com/comments'),
]);
$users = $responses['users']->json();
$posts = $responses['posts']->json();
```
## Fake HTTP Calls in Tests
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
Incorrect:
```php
it('syncs user from API', function () {
$service = new UserSyncService;
$service->sync(1); // Hits the real API
});
```
Correct:
```php
it('syncs user from API', function () {
Http::preventStrayRequests();
Http::fake([
'api.example.com/users/1' => Http::response([
'name' => 'John Doe',
'email' => 'john@example.com',
]),
]);
$service = new UserSyncService;
$service->sync(1);
Http::assertSent(function (Request $request) {
return $request->url() === 'https://api.example.com/users/1';
});
});
```
Test failure scenarios too:
```php
Http::fake([
'api.example.com/*' => Http::failedConnection(),
]);
```

View file

@ -0,0 +1,27 @@
# Mail Best Practices
## Implement `ShouldQueue` on the Mailable Class
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
## Use `afterCommit()` on Mailables Inside Transactions
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence.
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
Correct: `Mail::assertQueued(OrderShipped::class);`
## Use Markdown Mailables for Transactional Emails
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
## Separate Content Tests from Sending Tests
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
Don't mix them — it conflates concerns and makes tests brittle.

View file

@ -0,0 +1,121 @@
# Migration Best Practices
## Generate Migrations with Artisan
Always use `php artisan make:migration` for consistent naming and timestamps.
Incorrect (manually created file):
```php
// database/migrations/posts_migration.php ← wrong naming, no timestamp
```
Correct (Artisan-generated):
```bash
php artisan make:migration create_posts_table
php artisan make:migration add_slug_to_posts_table
```
## Use `constrained()` for Foreign Keys
Automatic naming and referential integrity.
```php
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
// Non-standard names
$table->foreignId('author_id')->constrained('users');
```
## Never Modify Deployed Migrations
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
Incorrect (editing a deployed migration):
```php
// 2024_01_01_create_posts_table.php — already in production
$table->string('slug')->unique(); // ← added after deployment
```
Correct (new migration to alter):
```php
// 2024_03_15_add_slug_to_posts_table.php
Schema::table('posts', function (Blueprint $table) {
$table->string('slug')->unique()->after('title');
});
```
## Add Indexes in the Migration
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
Incorrect:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
```
Correct:
```php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->index();
$table->string('status')->index();
$table->timestamp('shipped_at')->nullable()->index();
$table->timestamps();
});
```
## Mirror Defaults in Model `$attributes`
When a column has a database default, mirror it in the model so new instances have correct values before saving.
```php
// Migration
$table->string('status')->default('pending');
// Model
protected $attributes = [
'status' => 'pending',
];
```
## Write Reversible `down()` Methods by Default
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
```php
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
```
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
## Keep Migrations Focused
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
Incorrect (partial failure creates unrecoverable state):
```php
public function up(): void
{
Schema::create('settings', function (Blueprint $table) { ... });
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
}
```
Correct (separate migrations):
```php
// Migration 1: create_settings_table
Schema::create('settings', function (Blueprint $table) { ... });
// Migration 2: seed_default_settings
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
```

View file

@ -0,0 +1,146 @@
# Queue & Job Best Practices
## Set `retry_after` Greater Than `timeout`
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
Incorrect (`retry_after` ≤ `timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 90 ← job retried while still running!
```
Correct (`retry_after` > `timeout`):
```php
class ProcessReport implements ShouldQueue
{
public $timeout = 120;
}
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
```
## Use Exponential Backoff
Use progressively longer delays between retries to avoid hammering failing services.
Incorrect (fixed retry interval):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
// Default: retries immediately, overwhelming the API
}
```
Correct (exponential backoff):
```php
class SyncWithStripe implements ShouldQueue
{
public $tries = 3;
public $backoff = [1, 5, 10];
}
```
## Implement `ShouldBeUnique`
Prevent duplicate job processing.
```php
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->order->id;
}
public $uniqueFor = 3600;
}
```
## Always Implement `failed()`
Handle errors explicitly — don't rely on silent failure.
```php
public function failed(?Throwable $exception): void
{
$this->podcast->update(['status' => 'failed']);
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
}
```
## Rate Limit External API Calls in Jobs
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
```php
public function middleware(): array
{
return [new RateLimited('external-api')];
}
```
## Batch Related Jobs
Use `Bus::batch()` when jobs should succeed or fail together.
```php
Bus::batch([
new ImportCsvChunk($chunk1),
new ImportCsvChunk($chunk2),
])
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
->dispatch();
```
## `retryUntil()` Needs `$tries = 0`
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
```php
public $tries = 0;
public function retryUntil(): DateTime
{
return now()->addHours(4);
}
```
## Use `WithoutOverlapping::untilProcessing()`
Prevents concurrent execution while allowing new instances to queue.
```php
public function middleware(): array
{
return [new WithoutOverlapping($this->product->id)->untilProcessing()];
}
```
Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts.
## Use Horizon for Complex Queue Scenarios
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
```php
// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
],
```

View file

@ -0,0 +1,98 @@
# Routing & Controllers Best Practices
## Use Implicit Route Model Binding
Let Laravel resolve models automatically from route parameters.
Incorrect:
```php
public function show(int $id)
{
$post = Post::findOrFail($id);
}
```
Correct:
```php
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
```
## Use Scoped Bindings for Nested Resources
Enforce parent-child relationships automatically.
```php
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
// $post is automatically scoped to $user
})->scopeBindings();
```
## Use Resource Controllers
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
```php
Route::resource('posts', PostController::class);
Route::apiResource('api/posts', Api\PostController::class);
```
## Keep Controllers Thin
Aim for under 10 lines per method. Extract business logic to action or service classes.
Incorrect:
```php
public function store(Request $request)
{
$validated = $request->validate([...]);
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images'));
}
$post = Post::create($validated);
$post->tags()->sync($validated['tags']);
event(new PostCreated($post));
return redirect()->route('posts.show', $post);
}
```
Correct:
```php
public function store(StorePostRequest $request, CreatePostAction $create)
{
$post = $create->execute($request->validated());
return redirect()->route('posts.show', $post);
}
```
## Type-Hint Form Requests
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
Incorrect:
```php
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'max:255'],
'body' => ['required'],
]);
Post::create($validated);
return redirect()->route('posts.index');
}
```
Correct:
```php
public function store(StorePostRequest $request): RedirectResponse
{
Post::create($request->validated());
return redirect()->route('posts.index');
}
```

View file

@ -0,0 +1,39 @@
# Task Scheduling Best Practices
## Use `withoutOverlapping()` on Variable-Duration Tasks
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
## Use `onOneServer()` on Multi-Server Deployments
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
## Use `runInBackground()` for Concurrent Long Tasks
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
## Use `environments()` to Restrict Tasks
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
```php
Schedule::command('billing:charge')->monthly()->environments(['production']);
```
## Use `takeUntilTimeout()` for Time-Bounded Processing
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
## Use Schedule Groups for Shared Configuration
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
```php
Schedule::daily()
->onOneServer()
->timezone('America/New_York')
->group(function () {
Schedule::command('emails:send --force');
Schedule::command('emails:prune');
});
```

View file

@ -0,0 +1,198 @@
# Security Best Practices
## Mass Assignment Protection
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
Incorrect:
```php
class User extends Model
{
protected $guarded = []; // All fields are mass assignable
}
```
Correct:
```php
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
```
Never use `$guarded = []` on models that accept user input.
## Authorize Every Action
Use policies or gates in controllers. Never skip authorization.
Incorrect:
```php
public function update(Request $request, Post $post)
{
$post->update($request->validated());
}
```
Correct:
```php
public function update(UpdatePostRequest $request, Post $post)
{
Gate::authorize('update', $post);
$post->update($request->validated());
}
```
Or via Form Request:
```php
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
```
## Prevent SQL Injection
Always use parameter binding. Never interpolate user input into queries.
Incorrect:
```php
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
```
Correct:
```php
User::where('name', $request->name)->get();
// Raw expressions with bindings
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
```
## Escape Output to Prevent XSS
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
Incorrect:
```blade
{!! $user->bio !!}
```
Correct:
```blade
{{ $user->bio }}
```
## CSRF Protection
Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia.
Incorrect:
```blade
<form method="POST" action="/posts">
<input type="text" name="title">
</form>
```
Correct:
```blade
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
</form>
```
## Rate Limit Auth and API Routes
Apply `throttle` middleware to authentication and API routes.
```php
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Route::post('/login', LoginController::class)->middleware('throttle:login');
```
## Validate File Uploads
Validate MIME type, extension, and size. Never trust client-provided filenames.
```php
public function rules(): array
{
return [
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
];
}
```
Store with generated filenames:
```php
$path = $request->file('avatar')->store('avatars', 'public');
```
## Keep Secrets Out of Code
Never commit `.env`. Access secrets via `config()` only.
Incorrect:
```php
$key = env('API_KEY');
```
Correct:
```php
// config/services.php
'api_key' => env('API_KEY'),
// In application code
$key = config('services.api_key');
```
## Audit Dependencies
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
```bash
composer audit
```
## Encrypt Sensitive Database Fields
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
Incorrect:
```php
class Integration extends Model
{
protected function casts(): array
{
return [
'api_key' => 'string',
];
}
}
```
Correct:
```php
class Integration extends Model
{
protected $hidden = ['api_key', 'api_secret'];
protected function casts(): array
{
return [
'api_key' => 'encrypted',
'api_secret' => 'encrypted',
];
}
}
```

Binary file not shown.

View file

@ -0,0 +1,43 @@
# Testing Best Practices
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites.
## Use Model Assertions Over Raw Database Assertions
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
Correct: `$this->assertModelExists($user);`
More expressive, type-safe, and fails with clearer messages.
## Use Factory States and Sequences
Named states make tests self-documenting. Sequences eliminate repetitive setup.
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
Correct: `User::factory()->unverified()->create();`
## Use `Exceptions::fake()` to Assert Exception Reporting
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
## Call `Event::fake()` After Factory Setup
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
Incorrect: `Event::fake(); $user = User::factory()->create();`
Correct: `$user = User::factory()->create(); Event::fake();`
## Use `recycle()` to Share Relationship Instances Across Factories
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
```php
Ticket::factory()
->recycle(Airline::factory()->create())
->create();
```

View file

@ -0,0 +1,75 @@
# Validation & Forms Best Practices
## Use Form Request Classes
Extract validation from controllers into dedicated Form Request classes.
Incorrect:
```php
public function store(Request $request)
{
$request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
}
```
Correct:
```php
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
```
## Array vs. String Notation for Rules
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
```php
// Preferred for new code
'email' => ['required', 'email', Rule::unique('users')],
// Follow existing convention if the project uses string notation
'email' => 'required|email|unique:users',
```
## Always Use `validated()`
Get only validated data. Never use `$request->all()` for mass operations.
Incorrect:
```php
Post::create($request->all());
```
Correct:
```php
Post::create($request->validated());
```
## Use `Rule::when()` for Conditional Validation
```php
'company_name' => [
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
],
```
## Use the `after()` Method for Custom Validation
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
```php
public function after(): array
{
return [
function (Validator $validator) {
if ($this->quantity > Product::find($this->product_id)?->stock) {
$validator->errors()->add('quantity', 'Not enough stock.');
}
},
];
}
```

View file

@ -1,24 +1,13 @@
---
name: livewire-development
description: >-
Develops reactive Livewire 3 components. Activates when creating, updating, or modifying
Livewire components; working with wire:model, wire:click, wire:loading, or any wire: directives;
adding real-time updates, loading states, or reactivity; debugging component behavior;
writing Livewire tests; or when the user mentions Livewire, component, counter, or reactive UI.
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, loading states, migrating from Livewire 2 to 3, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
license: MIT
metadata:
author: laravel
---
# Livewire Development
## When to Apply
Activate this skill when:
- Creating new Livewire components
- Modifying existing component state or behavior
- Debugging reactivity or lifecycle issues
- Writing Livewire component tests
- Adding Alpine.js interactivity to components
- Working with wire: directives
## Documentation
Use `search-docs` for detailed Livewire 3 patterns and documentation.
@ -62,33 +51,31 @@ ### Component Structure
### Using Keys in Loops
<code-snippet name="Wire Key in Loops" lang="blade">
<!-- Wire Key in Loops -->
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
</code-snippet>
```
### Lifecycle Hooks
Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle Hook Examples" lang="php">
<!-- Lifecycle Hook Examples -->
```php
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
```
## JavaScript Hooks
You can listen for `livewire:init` to hook into Livewire initialization:
<code-snippet name="Livewire Init Hook Example" lang="js">
<!-- Livewire Init Hook Example -->
```js
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
@ -100,28 +87,25 @@ ## JavaScript Hooks
console.error(message);
});
});
</code-snippet>
```
## Testing
<code-snippet name="Example Livewire Component Test" lang="php">
<!-- Example Livewire Component Test -->
```php
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
```
</code-snippet>
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
<!-- Testing Livewire Component Exists on Page -->
```php
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
```
## Common Pitfalls

View file

@ -1,63 +1,55 @@
---
name: pest-testing
description: >-
Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature
tests, adding assertions, testing Livewire components, browser testing, debugging test failures,
working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion,
coverage, or needs to verify functionality works.
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
# Pest Testing 4
## When to Apply
Activate this skill when:
- Creating new tests (unit, feature, or browser)
- Modifying existing tests
- Debugging test failures
- Working with browser testing or smoke testing
- Writing architecture tests or visual regression tests
## Documentation
Use `search-docs` for detailed Pest 4 patterns and documentation.
## Test Directory Structure
## Basic Usage
- `tests/Feature/` and `tests/Unit/` — Legacy tests (keep, don't delete)
- `tests/v4/Feature/` — New feature tests (SQLite :memory: database)
- `tests/v4/Browser/` — Browser tests (Pest Browser Plugin + Playwright)
- `tests/Browser/` — Legacy Dusk browser tests (keep, don't delete)
### Creating Tests
New tests go in `tests/v4/`. The v4 suite uses SQLite :memory: with a schema dump (`database/schema/testing-schema.sql`) instead of running migrations.
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
Do NOT remove tests without approval.
### Test Organization
## Running Tests
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
- All v4 tests: `php artisan test --compact tests/v4/`
- Browser tests: `php artisan test --compact tests/v4/Browser/`
- Feature tests: `php artisan test --compact tests/v4/Feature/`
- Specific file: `php artisan test --compact tests/v4/Browser/LoginTest.php`
- Filter: `php artisan test --compact --filter=testName`
- Headed (see browser): `./vendor/bin/pest tests/v4/Browser/ --headed`
- Debug (pause on failure): `./vendor/bin/pest tests/v4/Browser/ --debug`
## Basic Test Structure
<code-snippet name="Basic Pest Test Example" lang="php">
### Basic Test Structure
<!-- Basic Pest Test Example -->
```php
it('is true', function () {
expect(true)->toBeTrue();
});
```
</code-snippet>
### Running Tests
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
<!-- Pest Response Assertion -->
```php
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
```
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
@ -70,116 +62,91 @@ ## Mocking
## Datasets
Use datasets for repetitive tests:
<code-snippet name="Pest Dataset Example" lang="php">
Use datasets for repetitive tests (validation rules, etc.):
<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
## Browser Testing (Pest Browser Plugin + Playwright)
Browser tests use `pestphp/pest-plugin-browser` with Playwright. They run **outside Docker** — the plugin starts an in-process HTTP server and Playwright browser automatically.
### Key Rules
1. **Always use `RefreshDatabase`** — the in-process server uses SQLite :memory:
2. **Always seed `InstanceSettings::create(['id' => 0])` in `beforeEach`** — most pages crash without it
3. **Use `User::factory()` for auth tests** — create users with `id => 0` for root user
4. **No Dusk, no Selenium** — use `visit()`, `fill()`, `click()`, `assertSee()` from the Pest Browser API
5. **Place tests in `tests/v4/Browser/`**
6. **Views with bare `function` declarations** will crash on the second request in the same process — wrap with `function_exists()` guard if you encounter this
### Browser Test Template
<code-snippet name="Coolify Browser Test Template" lang="php">
<?php
use App\Models\InstanceSettings;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::create(['id' => 0]);
});
it('can visit the page', function () {
$page = visit('/login');
$page->assertSee('Login');
});
</code-snippet>
### Browser Test with Form Interaction
<code-snippet name="Browser Test Form Example" lang="php">
it('fails login with invalid credentials', function () {
User::factory()->create([
'id' => 0,
'email' => 'test@example.com',
'password' => Hash::make('password'),
]);
$page = visit('/login');
$page->fill('email', 'random@email.com')
->fill('password', 'wrongpassword123')
->click('Login')
->assertSee('These credentials do not match our records');
});
</code-snippet>
### Browser API Reference
| Method | Purpose |
|--------|---------|
| `visit('/path')` | Navigate to a page |
| `->fill('field', 'value')` | Fill an input by name |
| `->click('Button Text')` | Click a button/link by text |
| `->assertSee('text')` | Assert visible text |
| `->assertDontSee('text')` | Assert text is not visible |
| `->assertPathIs('/path')` | Assert current URL path |
| `->assertSeeIn('.selector', 'text')` | Assert text in element |
| `->screenshot()` | Capture screenshot |
| `->debug()` | Pause test, keep browser open |
| `->wait(seconds)` | Wait N seconds |
### Debugging
- Screenshots auto-saved to `tests/Browser/Screenshots/` on failure
- `->debug()` pauses and keeps browser open (press Enter to continue)
- `->screenshot()` captures state at any point
- `--headed` flag shows browser, `--debug` pauses on failure
## SQLite Testing Setup
v4 tests use SQLite :memory: instead of PostgreSQL. Schema loaded from `database/schema/testing-schema.sql`.
### Regenerating the Schema
When migrations change, regenerate from the running PostgreSQL database:
```bash
docker exec coolify php artisan schema:generate-testing
```
## Architecture Testing
## Pest 4 Features
<code-snippet name="Architecture Test Example" lang="php">
| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
```
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Test Sharding
Split tests across parallel processes for faster CI runs.
### Architecture Testing
Pest 4 includes architecture testing (from Pest 3):
<!-- Architecture Test Example -->
```php
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
</code-snippet>
```
## Common Pitfalls
@ -187,7 +154,4 @@ ## Common Pitfalls
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests
- **Browser tests: forgetting `InstanceSettings::create(['id' => 0])` — most pages crash without it**
- **Browser tests: forgetting `RefreshDatabase` — SQLite :memory: starts empty**
- **Browser tests: views with bare `function` declarations crash on second request — wrap with `function_exists()` guard**
- Forgetting `assertNoJavaScriptErrors()` in browser tests

View file

@ -0,0 +1,80 @@
---
name: socialite-development
description: "Manages OAuth social authentication with Laravel Socialite. Activate when adding social login providers; configuring OAuth redirect/callback flows; retrieving authenticated user details; customizing scopes or parameters; setting up community providers; testing with Socialite fakes; or when the user mentions social login, OAuth, Socialite, or third-party authentication."
license: MIT
metadata:
author: laravel
---
# Socialite Authentication
## Documentation
Use `search-docs` for detailed Socialite patterns and documentation (installation, configuration, routing, callbacks, testing, scopes, stateless auth).
## Available Providers
Built-in: `facebook`, `twitter`, `twitter-oauth-2`, `linkedin`, `linkedin-openid`, `google`, `github`, `gitlab`, `bitbucket`, `slack`, `slack-openid`, `twitch`
Community: 150+ additional providers at [socialiteproviders.com](https://socialiteproviders.com). For provider-specific setup, use `WebFetch` on `https://socialiteproviders.com/{provider-name}`.
Configuration key in `config/services.php` must match the driver name exactly — note the hyphenated keys: `twitter-oauth-2`, `linkedin-openid`, `slack-openid`.
Twitter/X: Use `twitter-oauth-2` (OAuth 2.0) for new projects. The legacy `twitter` driver is OAuth 1.0. Driver names remain unchanged despite the platform rebrand.
Community providers differ from built-in providers in the following ways:
- Installed via `composer require socialiteproviders/{name}`
- Must register via event listener — NOT auto-discovered like built-in providers
- Use `search-docs` for the registration pattern
## Adding a Provider
### 1. Configure the provider
Add the provider's `client_id`, `client_secret`, and `redirect` to `config/services.php`. The config key must match the driver name exactly.
### 2. Create redirect and callback routes
Two routes are needed: one that calls `Socialite::driver('provider')->redirect()` to send the user to the OAuth provider, and one that calls `Socialite::driver('provider')->user()` to receive the callback and retrieve user details.
### 3. Authenticate and store the user
In the callback, use `updateOrCreate` to find or create a user record from the provider's response (`id`, `name`, `email`, `token`, `refreshToken`), then call `Auth::login()`.
### 4. Customize the redirect (optional)
- `scopes()` — merge additional scopes with the provider's defaults
- `setScopes()` — replace all scopes entirely
- `with()` — pass optional parameters (e.g., `['hd' => 'example.com']` for Google)
- `asBotUser()` — Slack only; generates a bot token (`xoxb-`) instead of a user token (`xoxp-`). Must be called before both `redirect()` and `user()`. Only the `token` property will be hydrated on the user object.
- `stateless()` — for API/SPA contexts where session state is not maintained
### 5. Verify
1. Config key matches driver name exactly (check the list above for hyphenated names)
2. `client_id`, `client_secret`, and `redirect` are all present
3. Redirect URL matches what is registered in the provider's OAuth dashboard
4. Callback route handles denied grants (when user declines authorization)
Use `search-docs` for complete code examples of each step.
## Additional Features
Use `search-docs` for usage details on: `enablePKCE()`, `userFromToken($token)`, `userFromTokenAndSecret($token, $secret)` (OAuth 1.0), retrieving user details.
User object: `getId()`, `getName()`, `getEmail()`, `getAvatar()`, `getNickname()`, `token`, `refreshToken`, `expiresIn`, `approvedScopes`
## Testing
Socialite provides `Socialite::fake()` for testing redirects and callbacks. Use `search-docs` for faking redirects, callback user data, custom token properties, and assertion methods.
## Common Pitfalls
- Config key must match driver name exactly — hyphenated drivers need hyphenated keys (`linkedin-openid`, `slack-openid`, `twitter-oauth-2`). Mismatch silently fails.
- Every provider needs `client_id`, `client_secret`, and `redirect` in `config/services.php`. Missing any one causes cryptic errors.
- `scopes()` merges with defaults; `setScopes()` replaces all scopes entirely.
- Missing `stateless()` in API/SPA contexts causes `InvalidStateException`.
- Redirect URL in `config/services.php` must exactly match the provider's OAuth dashboard (including trailing slashes and protocol).
- Do not pass `state`, `response_type`, `client_id`, `redirect_uri`, or `scope` via `with()` — these are reserved.
- Community providers require event listener registration via `SocialiteWasCalled`.
- `user()` throws when the user declines authorization. Always handle denied grants.

View file

@ -1,24 +1,13 @@
---
name: tailwindcss-development
description: >-
Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components,
working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors,
typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle,
hero section, cards, buttons, or any visual/UI changes.
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
author: laravel
---
# Tailwind CSS Development
## When to Apply
Activate this skill when:
- Adding styles to components or pages
- Working with responsive design
- Implementing dark mode
- Extracting repeated patterns into components
- Debugging spacing or layout issues
## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
@ -38,22 +27,24 @@ ### CSS-First Configuration
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<code-snippet name="CSS-First Config" lang="css">
<!-- CSS-First Config -->
```css
@theme {
--color-brand: oklch(0.72 0.11 178);
}
</code-snippet>
```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<code-snippet name="v4 Import Syntax" lang="diff">
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
```
### Replaced Utilities
@ -77,43 +68,47 @@ ## Spacing
Use `gap` utilities instead of margins for spacing between siblings:
<code-snippet name="Gap Utilities" lang="html">
<!-- Gap Utilities -->
```html
<div class="flex gap-8">
<div>Item 1</div>
<div>Item 2</div>
</div>
</code-snippet>
```
## Dark Mode
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
<code-snippet name="Dark Mode" lang="html">
<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content adapts to color scheme
</div>
</code-snippet>
```
## Common Patterns
### Flexbox Layout
<code-snippet name="Flexbox Layout" lang="html">
<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
<div>Left content</div>
<div>Right content</div>
</div>
</code-snippet>
```
### Grid Layout
<code-snippet name="Grid Layout" lang="html">
<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
</code-snippet>
```
## Common Pitfalls

View file

@ -0,0 +1,85 @@
---
name: configuring-horizon
description: "Use this skill whenever the user mentions Horizon by name in a Laravel context. Covers the full Horizon lifecycle: installing Horizon (horizon:install, Sail setup), configuring config/horizon.php (supervisor blocks, queue assignments, balancing strategies, minProcesses/maxProcesses), fixing the dashboard (authorization via Gate::define viewHorizon, blank metrics, horizon:snapshot scheduling), and troubleshooting production issues (worker crashes, timeout chain ordering, LongWaitDetected notifications, waits config). Also covers job tagging and silencing. Do not use for generic Laravel queues without Horizon, SQS or database drivers, standalone Redis setup, Linux supervisord, Telescope, or job batching."
license: MIT
metadata:
author: laravel
---
# Horizon Configuration
## Documentation
Use `search-docs` for detailed Horizon patterns and documentation covering configuration, supervisors, balancing, dashboard authorization, tags, notifications, metrics, and deployment.
For deeper guidance on specific topics, read the relevant reference file before implementing:
- `references/supervisors.md` covers supervisor blocks, balancing strategies, multi-queue setups, and auto-scaling
- `references/notifications.md` covers LongWaitDetected alerts, notification routing, and the `waits` config
- `references/tags.md` covers job tagging, dashboard filtering, and silencing noisy jobs
- `references/metrics.md` covers the blank metrics dashboard, snapshot scheduling, and retention config
## Basic Usage
### Installation
```bash
php artisan horizon:install
```
### Supervisor Configuration
Define supervisors in `config/horizon.php`. The `environments` array merges into `defaults` and does not replace the whole supervisor block:
<!-- Supervisor Config -->
```php
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
'environments' => [
'production' => [
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
],
'local' => [
'supervisor-1' => ['maxProcesses' => 2],
],
],
```
### Dashboard Authorization
Restrict access in `App\Providers\HorizonServiceProvider`:
<!-- Dashboard Gate -->
```php
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
return $user->is_admin;
});
}
```
## Verification
1. Run `php artisan horizon` and visit `/horizon`
2. Confirm dashboard access is restricted as expected
3. Check that metrics populate after scheduling `horizon:snapshot`
## Common Pitfalls
- Horizon only works with the Redis queue driver. Other drivers such as database and SQS are not supported.
- Redis Cluster is not supported. Horizon requires a standalone Redis connection.
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.

View file

@ -0,0 +1,21 @@
# Metrics & Snapshots
## Where to Find It
Search with `search-docs`:
- `"horizon metrics snapshot"` for the snapshot command and scheduling
- `"horizon trim snapshots"` for retention configuration
## What to Watch For
### Metrics dashboard stays blank until `horizon:snapshot` is scheduled
Running `horizon` artisan command does not populate metrics automatically. The metrics graph is built from snapshots, so `horizon:snapshot` must be scheduled to run every 5 minutes via Laravel's scheduler.
### Register the snapshot in the scheduler rather than running it manually
A single manual run populates the dashboard momentarily but will not keep it updated. Search `"horizon metrics snapshot"` for the exact scheduler registration syntax, which differs between Laravel 10 and 11+.
### `metrics.trim_snapshots` is a snapshot count, not a time duration
The `trim_snapshots.job` and `trim_snapshots.queue` values in `config/horizon.php` are counts of snapshots to keep, not minutes or hours. With the default of 24 snapshots at 5-minute intervals, that provides 2 hours of history. Increase the value to retain more history at the cost of Redis memory usage.

View file

@ -0,0 +1,21 @@
# Notifications & Alerts
## Where to Find It
Search with `search-docs`:
- `"horizon notifications"` for Horizon's built-in notification routing helpers
- `"horizon long wait detected"` for LongWaitDetected event details
## What to Watch For
### `waits` in `config/horizon.php` controls the LongWaitDetected threshold
The `waits` array (e.g., `'redis:default' => 60`) defines how many seconds a job can wait in a queue before Horizon fires a `LongWaitDetected` event. This value is set in the config file, not in Horizon's notification routing. If alerts are firing too often or too late, adjust `waits` rather than the routing configuration.
### Use Horizon's built-in notification routing in `HorizonServiceProvider`
Configure notifications in the `boot()` method of `App\Providers\HorizonServiceProvider` using `Horizon::routeMailNotificationsTo()`, `Horizon::routeSlackNotificationsTo()`, or `Horizon::routeSmsNotificationsTo()`. Horizon already wires `LongWaitDetected` to its notification sender, so the documented setup is notification routing rather than manual listener registration.
### Failed job alerts are separate from Horizon's documented notification routing
Horizon's 12.x documentation covers built-in long-wait notifications. Do not assume the docs provide a `JobFailed` listener example in `HorizonServiceProvider`. If a user needs failed job alerts, treat that as custom queue event handling and consult the queue documentation instead of Horizon's notification-routing API.

View file

@ -0,0 +1,27 @@
# Supervisor & Balancing Configuration
## Where to Find It
Search with `search-docs` before writing any supervisor config, as option names and defaults change between Horizon versions:
- `"horizon supervisor configuration"` for the full options list
- `"horizon balancing strategies"` for auto, simple, and false modes
- `"horizon autoscaling workers"` for autoScalingStrategy details
- `"horizon environment configuration"` for the defaults and environments merge
## What to Watch For
### The `environments` array merges into `defaults` rather than replacing it
The `defaults` array defines the complete base supervisor config. The `environments` array patches it per environment, overriding only the keys listed. There is no need to repeat every key in each environment block. A common pattern is to define `connection`, `queue`, `balance`, `autoScalingStrategy`, `tries`, and `timeout` in `defaults`, then override only `maxProcesses`, `balanceMaxShift`, and `balanceCooldown` in `production`.
### Use separate named supervisors to enforce queue priority
Horizon does not enforce queue order when using `balance: auto` on a single supervisor. The `queue` array order is ignored for load balancing. To process `notifications` before `default`, use two separately named supervisors: one for the high-priority queue with a higher `maxProcesses`, and one for the low-priority queue with a lower cap. The docs include an explicit note about this.
### Use `balance: false` to keep a fixed number of workers on a dedicated queue
Auto-balancing suits variable load, but if a queue should always have exactly N workers such as a video-processing queue limited to 2, set `balance: false` and `maxProcesses: 2`. Auto-balancing would scale it up during bursts, which may be undesirable.
### Set `balanceCooldown` to prevent rapid worker scaling under bursty load
When using `balance: auto`, the supervisor can scale up and down rapidly under bursty load. Set `balanceCooldown` to the number of seconds between scaling decisions, typically 3 to 5, to smooth this out. `balanceMaxShift` limits how many processes are added or removed per cycle.

View file

@ -0,0 +1,21 @@
# Tags & Silencing
## Where to Find It
Search with `search-docs`:
- `"horizon tags"` for the tagging API and auto-tagging behaviour
- `"horizon silenced jobs"` for the `silenced` and `silenced_tags` config options
## What to Watch For
### Eloquent model jobs are tagged automatically without any extra code
If a job's constructor accepts Eloquent model instances, Horizon automatically tags the job with `ModelClass:id` such as `App\Models\User:42`. These tags are filterable in the dashboard without any changes to the job class. Only add a `tags()` method when custom tags beyond auto-tagging are needed.
### `silenced` hides jobs from the dashboard completed list but does not stop them from running
Adding a job class to the `silenced` array in `config/horizon.php` removes it from the completed jobs view. The job still runs normally. This is a dashboard noise-reduction tool, not a way to disable jobs.
### `silenced_tags` hides all jobs carrying a matching tag from the completed list
Any job carrying a matching tag string is hidden from the completed jobs view. This is useful for silencing a category of jobs such as all jobs tagged `notifications`, rather than silencing specific classes.

View file

@ -1,6 +1,9 @@
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
name: fortify-development
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
license: MIT
metadata:
author: laravel
---
# Laravel Fortify Development
@ -39,7 +42,7 @@ ### Two-Factor Authentication Setup
```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
@ -75,14 +78,26 @@ ### SPA Authentication Setup
```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```
> Use `search-docs` for integration and SPA authentication patterns.
#### Two-Factor Authentication in SPA Mode
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
```json
{
"two_factor": true
}
```
## Best Practices
### Custom Authentication Logic

View file

@ -0,0 +1,302 @@
---
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.
---
# Laravel Actions or `lorisleiva/laravel-actions`
## Overview
Use this skill to implement or update actions based on `lorisleiva/laravel-actions` with consistent structure and predictable testing patterns.
## Quick Workflow
1. Confirm the package is installed with `composer show lorisleiva/laravel-actions`.
2. Create or edit an action class that uses `Lorisleiva\Actions\Concerns\AsAction`.
3. Implement `handle(...)` with the core business logic first.
4. Add adapter methods only when needed for the requested entrypoint:
- `asController` (+ route/invokable controller usage)
- `asJob` (+ dispatch)
- `asListener` (+ event listener wiring)
- `asCommand` (+ command signature/description)
5. Add or update tests for the chosen entrypoint.
6. When tests need isolation, use action fakes (`MyAction::fake()`) and assertions (`MyAction::assertDispatched()`).
## Base Action Pattern
Use this minimal skeleton and expand only what is needed.
```php
<?php
namespace App\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
return true;
}
}
```
## Project Conventions
- Place action classes in `App\Actions` unless an existing domain sub-namespace is already used.
- Use descriptive `VerbNoun` naming (e.g. `PublishArticle`, `SyncVehicleTaxStatus`).
- Keep domain/business logic in `handle(...)`; keep transport and framework concerns in adapter methods (`asController`, `asJob`, `asListener`, `asCommand`).
- Prefer explicit parameter and return types in all action methods.
- Prefer PHPDoc for complex data contracts (e.g. array shapes), not inline comments.
### When to Use an Action
- Use an Action when the same use-case needs multiple entrypoints (HTTP, queue, event, CLI) or benefits from first-class orchestration/faking.
- Keep a plain service class when logic is local, single-entrypoint, and unlikely to be reused as an Action.
## Entrypoint Patterns
### Run as Object
- (prefer method) Use static helper from the trait: `PublishArticle::run($id)`.
- Use make and call handle: `PublishArticle::make()->handle($id)`.
- Call with dependency injection: `app(PublishArticle::class)->handle($id)`.
### Run as Controller
- Use route to class (invokable style), e.g. `Route::post('/articles/{id}/publish', PublishArticle::class)`.
- Add `asController(...)` for HTTP-specific adaptation and return a response.
- Add request validation (`rules()` or custom validator hooks) when input comes from HTTP.
### Run as Job
- Dispatch with `PublishArticle::dispatch($id)`.
- Use `asJob(...)` only for queue-specific behavior; keep domain logic in `handle(...)`.
- In this project, job Actions often define additional queue lifecycle methods and job properties for retries, uniqueness, and timing control.
#### Project Pattern: Job Action with Extra Methods
```php
<?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.
### Run as Listener
- Register the action class as listener in `EventServiceProvider`.
- Use `asListener(EventName $event)` and delegate to `handle(...)`.
### Run as Command
- Define `$commandSignature` and `$commandDescription` properties.
- Implement `asCommand(Command $command)` and keep console IO in this method only.
- Import `Command` with `use Illuminate\Console\Command;`.
## Testing Guidance
Use a two-layer strategy:
1. `handle(...)` tests for business correctness.
2. entrypoint tests (`asController`, `asJob`, `asListener`, `asCommand`) for wiring/orchestration.
### Deep Dive: `AsFake` methods (2.x)
Reference: https://www.laravelactions.com/2.x/as-fake.html
Use these methods intentionally based on what you want to prove.
#### `mock()`
- Replaces the action with a full mock.
- Best when you need strict expectations and argument assertions.
```php
PublishArticle::mock()
->shouldReceive('handle')
->once()
->with(42)
->andReturnTrue();
```
#### `partialMock()`
- Replaces the action with a partial mock.
- Best when you want to keep most real behavior but stub one expensive/internal method.
```php
PublishArticle::partialMock()
->shouldReceive('fetchRemoteData')
->once()
->andReturn(['ok' => true]);
```
#### `spy()`
- Replaces the action with a spy.
- Best for post-execution verification ("was called with X") without predefining all expectations.
```php
$spy = PublishArticle::spy()->allows('handle')->andReturnTrue();
// execute code that triggers the action...
$spy->shouldHaveReceived('handle')->with(42);
```
#### `shouldRun()`
- Shortcut for `mock()->shouldReceive('handle')`.
- Best for compact orchestration assertions.
```php
PublishArticle::shouldRun()->once()->with(42)->andReturnTrue();
```
#### `shouldNotRun()`
- Shortcut for `mock()->shouldNotReceive('handle')`.
- Best for guard-clause tests and branch coverage.
```php
PublishArticle::shouldNotRun();
```
#### `allowToRun()`
- Shortcut for spy + allowing `handle`.
- Best when you want execution to proceed but still assert interaction.
```php
$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.
```php
expect(PublishArticle::isFake())->toBeFalse();
PublishArticle::mock();
expect(PublishArticle::isFake())->toBeTrue();
PublishArticle::clearFake();
expect(PublishArticle::isFake())->toBeFalse();
```
### Recommended test matrix for Actions
- Business rule test: call `handle(...)` directly with real dependencies/factories.
- HTTP wiring test: hit route/controller, fake downstream actions with `shouldRun` or `shouldNotRun`.
- Job wiring test: dispatch action as job, assert expected downstream action calls.
- Event listener test: dispatch event, assert action interaction via fake/spy.
- Console test: run artisan command, assert action invocation and output.
### Practical defaults
- Prefer `shouldRun()` and `shouldNotRun()` for readability in branch tests.
- Prefer `spy()`/`allowToRun()` when behavior is mostly real and you only need call verification.
- Prefer `mock()` when interaction contracts are strict and should fail fast.
- Use `clearFake()` in cleanup when a fake might leak into another test.
- Keep side effects isolated: fake only the action under test boundary, not everything.
### Pest style examples
```php
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.
## Troubleshooting Checklist
- Ensure the class uses `AsAction` and namespace matches autoload.
- Check route registration when used as controller.
- Check queue config when using `dispatch`.
- Verify event-to-listener mapping in `EventServiceProvider`.
- Keep transport concerns in adapter methods (`asController`, `asCommand`, etc.), not in `handle(...)`.
## Common Pitfalls
- Putting HTTP response/redirect logic inside `handle(...)` instead of `asController(...)`.
- Duplicating business rules across `as*` methods rather than delegating to `handle(...)`.
- Assuming listener wiring works without explicit registration where required.
- Testing only entrypoints and skipping direct `handle(...)` behavior tests.
- Overusing Actions for one-off, single-context logic with no reuse pressure.
## Topic References
Use these references for deep dives by entrypoint/topic. Keep `SKILL.md` focused on workflow and decision rules.
- Object entrypoint: `references/object.md`
- Controller entrypoint: `references/controller.md`
- Job entrypoint: `references/job.md`
- Listener entrypoint: `references/listener.md`
- Command entrypoint: `references/command.md`
- With attributes: `references/with-attributes.md`
- Testing and fakes: `references/testing-fakes.md`
- Troubleshooting: `references/troubleshooting.md`

View file

@ -0,0 +1,160 @@
# Command Entrypoint (`asCommand`)
## Scope
Use this reference when exposing actions as Artisan commands.
## Recap
- Documents command execution via `asCommand(...)` and fallback to `handle(...)`.
- Covers command metadata via methods/properties (signature, description, help, hidden).
- Includes registration example and focused artisan test pattern.
- Reinforces separation between console I/O and domain logic.
## Recommended pattern
- Define `$commandSignature` and `$commandDescription`.
- Implement `asCommand(Command $command)` for console I/O.
- Keep business logic in `handle(...)`.
## Methods used (`CommandDecorator`)
### `asCommand`
Called when executed as a command. If missing, it falls back to `handle(...)`.
```php
use Illuminate\Console\Command;
class UpdateUserRole
{
use AsAction;
public string $commandSignature = 'users:update-role {user_id} {role}';
public function handle(User $user, string $newRole): void
{
$user->update(['role' => $newRole]);
}
public function asCommand(Command $command): void
{
$this->handle(
User::findOrFail($command->argument('user_id')),
$command->argument('role')
);
$command->info('Done!');
}
}
```
### `getCommandSignature`
Defines the command signature. Required when registering an action as a command if no `$commandSignature` property is set.
```php
public function getCommandSignature(): string
{
return 'users:update-role {user_id} {role}';
}
```
### `$commandSignature`
Property alternative to `getCommandSignature`.
```php
public string $commandSignature = 'users:update-role {user_id} {role}';
```
### `getCommandDescription`
Provides command description.
```php
public function getCommandDescription(): string
{
return 'Updates the role of a given user.';
}
```
### `$commandDescription`
Property alternative to `getCommandDescription`.
```php
public string $commandDescription = 'Updates the role of a given user.';
```
### `getCommandHelp`
Provides additional help text shown with `--help`.
```php
public function getCommandHelp(): string
{
return 'My help message.';
}
```
### `$commandHelp`
Property alternative to `getCommandHelp`.
```php
public string $commandHelp = 'My help message.';
```
### `isCommandHidden`
Defines whether command should be hidden from artisan list. Default is `false`.
```php
public function isCommandHidden(): bool
{
return true;
}
```
### `$commandHidden`
Property alternative to `isCommandHidden`.
```php
public bool $commandHidden = true;
```
## Examples
### Register in console kernel
```php
// app/Console/Kernel.php
protected $commands = [
UpdateUserRole::class,
];
```
### Focused command test
```php
$this->artisan('users:update-role 1 admin')
->expectsOutput('Done!')
->assertSuccessful();
```
## Checklist
- `use Illuminate\Console\Command;` is imported.
- Signature/options/arguments are documented.
- Command test verifies invocation and output.
## Common pitfalls
- Mixing command I/O with domain logic in `handle(...)`.
- Missing/ambiguous command signature.
## References
- https://www.laravelactions.com/2.x/as-command.html

View file

@ -0,0 +1,339 @@
# Controller Entrypoint (`asController`)
## Scope
Use this reference when exposing an action through HTTP routes.
## Recap
- Documents controller lifecycle around `asController(...)` and response adapters.
- Covers routing patterns, middleware, and optional in-action `routes()` registration.
- Summarizes validation/authorization hooks used by `ActionRequest`.
- Provides extension points for JSON/HTML responses and failure customization.
## Recommended pattern
- Route directly to action class when appropriate.
- Keep HTTP adaptation in controller methods (`asController`, `jsonResponse`, `htmlResponse`).
- Keep domain logic in `handle(...)`.
## Methods provided (`AsController` trait)
### `__invoke`
Required so Laravel can register the action class as an invokable controller.
```php
$action($someArguments);
// Equivalent to:
$action->handle($someArguments);
```
If the method does not exist, Laravel route registration fails for invokable controllers.
```php
// Illuminate\Routing\RouteAction
protected static function makeInvokable($action)
{
if (! method_exists($action, '__invoke')) {
throw new UnexpectedValueException("Invalid route action: [{$action}].");
}
return $action.'@__invoke';
}
```
If you need your own `__invoke`, alias the trait implementation:
```php
class MyAction
{
use AsAction {
__invoke as protected invokeFromLaravelActions;
}
public function __invoke()
{
// Custom behavior...
}
}
```
## Methods used (`ControllerDecorator` + `ActionRequest`)
### `asController`
Called when used as invokable controller. If missing, it falls back to `handle(...)`.
```php
public function asController(User $user, Request $request): Response
{
$article = $this->handle(
$user,
$request->get('title'),
$request->get('body')
);
return redirect()->route('articles.show', [$article]);
}
```
### `jsonResponse`
Called after `asController` when request expects JSON.
```php
public function jsonResponse(Article $article, Request $request): ArticleResource
{
return new ArticleResource($article);
}
```
### `htmlResponse`
Called after `asController` when request expects HTML.
```php
public function htmlResponse(Article $article, Request $request): Response
{
return redirect()->route('articles.show', [$article]);
}
```
### `getControllerMiddleware`
Adds middleware directly on the action controller.
```php
public function getControllerMiddleware(): array
{
return ['auth', MyCustomMiddleware::class];
}
```
### `routes`
Defines routes directly in the action.
```php
public static function routes(Router $router)
{
$router->get('author/{author}/articles', static::class);
}
```
To enable this, register routes from actions in a service provider:
```php
use Lorisleiva\Actions\Facades\Actions;
Actions::registerRoutes();
Actions::registerRoutes('app/MyCustomActionsFolder');
Actions::registerRoutes([
'app/Authentication',
'app/Billing',
'app/TeamManagement',
]);
```
### `prepareForValidation`
Called before authorization and validation are resolved.
```php
public function prepareForValidation(ActionRequest $request): void
{
$request->merge(['some' => 'additional data']);
}
```
### `authorize`
Defines authorization logic.
```php
public function authorize(ActionRequest $request): bool
{
return $request->user()->role === 'author';
}
```
You can also return gate responses:
```php
use Illuminate\Auth\Access\Response;
public function authorize(ActionRequest $request): Response
{
if ($request->user()->role !== 'author') {
return Response::deny('You must be an author to create a new article.');
}
return Response::allow();
}
```
### `rules`
Defines validation rules.
```php
public function rules(): array
{
return [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
];
}
```
### `withValidator`
Adds custom validation logic with an after hook.
```php
use Illuminate\Validation\Validator;
public function withValidator(Validator $validator, ActionRequest $request): void
{
$validator->after(function (Validator $validator) use ($request) {
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
});
}
```
### `afterValidator`
Alternative to add post-validation checks.
```php
use Illuminate\Validation\Validator;
public function afterValidator(Validator $validator, ActionRequest $request): void
{
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
}
```
### `getValidator`
Provides a custom validator instead of default rules pipeline.
```php
use Illuminate\Validation\Factory;
use Illuminate\Validation\Validator;
public function getValidator(Factory $factory, ActionRequest $request): Validator
{
return $factory->make($request->only('title', 'body'), [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
]);
}
```
### `getValidationData`
Defines which data is validated (default: `$request->all()`).
```php
public function getValidationData(ActionRequest $request): array
{
return $request->all();
}
```
### `getValidationMessages`
Custom validation error messages.
```php
public function getValidationMessages(): array
{
return [
'title.required' => 'Looks like you forgot the title.',
'body.required' => 'Is that really all you have to say?',
];
}
```
### `getValidationAttributes`
Human-friendly names for request attributes.
```php
public function getValidationAttributes(): array
{
return [
'title' => 'headline',
'body' => 'content',
];
}
```
### `getValidationRedirect`
Custom redirect URL on validation failure.
```php
public function getValidationRedirect(UrlGenerator $url): string
{
return $url->to('/my-custom-redirect-url');
}
```
### `getValidationErrorBag`
Custom error bag name on validation failure (default: `default`).
```php
public function getValidationErrorBag(): string
{
return 'my_custom_error_bag';
}
```
### `getValidationFailure`
Override validation failure behavior.
```php
public function getValidationFailure(): void
{
throw new MyCustomValidationException();
}
```
### `getAuthorizationFailure`
Override authorization failure behavior.
```php
public function getAuthorizationFailure(): void
{
throw new MyCustomAuthorizationException();
}
```
## Checklist
- Route wiring points to the action class.
- `asController(...)` delegates to `handle(...)`.
- Validation/authorization methods are explicit where needed.
- Response mapping is split by channel (`jsonResponse`, `htmlResponse`) when useful.
- HTTP tests cover both success and validation/authorization failure branches.
## Common pitfalls
- Putting response/redirect logic in `handle(...)`.
- Duplicating business rules in `asController(...)` instead of delegating.
- Assuming action route discovery works without `Actions::registerRoutes(...)` when using in-action `routes()`.
## References
- https://www.laravelactions.com/2.x/as-controller.html

View file

@ -0,0 +1,425 @@
# Job Entrypoint (`dispatch`, `asJob`)
## Scope
Use this reference when running an action through queues.
## Recap
- Lists async/sync dispatch helpers and conditional dispatch variants.
- Covers job wrapping/chaining with `makeJob`, `makeUniqueJob`, and `withChain`.
- Documents queue assertion helpers for tests (`assertPushed*`).
- Summarizes `JobDecorator` hooks/properties for retries, uniqueness, timeout, and failure handling.
## Recommended pattern
- Dispatch with `Action::dispatch(...)` for async execution.
- Keep queue-specific orchestration in `asJob(...)`.
- Keep reusable business logic in `handle(...)`.
## Methods provided (`AsJob` trait)
### `dispatch`
Dispatches the action asynchronously.
```php
SendTeamReportEmail::dispatch($team);
```
### `dispatchIf`
Dispatches asynchronously only if condition is met.
```php
SendTeamReportEmail::dispatchIf($team->plan === 'premium', $team);
```
### `dispatchUnless`
Dispatches asynchronously unless condition is met.
```php
SendTeamReportEmail::dispatchUnless($team->plan === 'free', $team);
```
### `dispatchSync`
Dispatches synchronously.
```php
SendTeamReportEmail::dispatchSync($team);
```
### `dispatchNow`
Alias of `dispatchSync`.
```php
SendTeamReportEmail::dispatchNow($team);
```
### `dispatchAfterResponse`
Dispatches synchronously after the HTTP response is sent.
```php
SendTeamReportEmail::dispatchAfterResponse($team);
```
### `makeJob`
Creates a `JobDecorator` wrapper. Useful with `dispatch(...)` helper or chains.
```php
dispatch(SendTeamReportEmail::makeJob($team));
```
### `makeUniqueJob`
Creates a `UniqueJobDecorator` wrapper. Usually automatic with `ShouldBeUnique`, but can be forced.
```php
dispatch(SendTeamReportEmail::makeUniqueJob($team));
```
### `withChain`
Attaches jobs to run after successful processing.
```php
$chain = [
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
];
CreateNewTeamReport::withChain($chain)->dispatch($team);
```
Equivalent using `Bus::chain(...)`:
```php
use Illuminate\Support\Facades\Bus;
Bus::chain([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
])->dispatch();
```
Chain assertion example:
```php
use Illuminate\Support\Facades\Bus;
Bus::fake();
Bus::assertChained([
CreateNewTeamReport::makeJob($team),
OptimizeTeamReport::makeJob($team),
SendTeamReportEmail::makeJob($team),
]);
```
### `assertPushed`
Asserts the action was queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushed();
SendTeamReportEmail::assertPushed(3);
SendTeamReportEmail::assertPushed($callback);
SendTeamReportEmail::assertPushed(3, $callback);
```
`$callback` receives:
- Action instance.
- Dispatched arguments.
- `JobDecorator` instance.
- Queue name.
### `assertNotPushed`
Asserts the action was not queued.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertNotPushed();
SendTeamReportEmail::assertNotPushed($callback);
```
### `assertPushedOn`
Asserts the action was queued on a specific queue.
```php
use Illuminate\Support\Facades\Queue;
Queue::fake();
SendTeamReportEmail::assertPushedOn('reports');
SendTeamReportEmail::assertPushedOn('reports', 3);
SendTeamReportEmail::assertPushedOn('reports', $callback);
SendTeamReportEmail::assertPushedOn('reports', 3, $callback);
```
## Methods used (`JobDecorator`)
### `asJob`
Called when dispatched as a job. Falls back to `handle(...)` if missing.
```php
class SendTeamReportEmail
{
use AsAction;
public function handle(Team $team, bool $fullReport = false): void
{
// Prepare report and send it to all $team->users.
}
public function asJob(Team $team): void
{
$this->handle($team, true);
}
}
```
### `getJobMiddleware`
Adds middleware to the queued action.
```php
public function getJobMiddleware(array $parameters): array
{
return [new RateLimited('reports')];
}
```
### `configureJob`
Configures `JobDecorator` options.
```php
use Lorisleiva\Actions\Decorators\JobDecorator;
public function configureJob(JobDecorator $job): void
{
$job->onConnection('my_connection')
->onQueue('my_queue')
->through(['my_middleware'])
->chain(['my_chain'])
->delay(60);
}
```
### `$jobConnection`
Defines queue connection.
```php
public string $jobConnection = 'my_connection';
```
### `$jobQueue`
Defines queue name.
```php
public string $jobQueue = 'my_queue';
```
### `$jobTries`
Defines max attempts.
```php
public int $jobTries = 10;
```
### `$jobMaxExceptions`
Defines max unhandled exceptions before failure.
```php
public int $jobMaxExceptions = 3;
```
### `$jobBackoff`
Defines retry delay seconds.
```php
public int $jobBackoff = 60;
```
### `getJobBackoff`
Defines retry delay (int or per-attempt array).
```php
public function getJobBackoff(): int
{
return 60;
}
public function getJobBackoff(): array
{
return [30, 60, 120];
}
```
### `$jobTimeout`
Defines timeout in seconds.
```php
public int $jobTimeout = 60 * 30;
```
### `$jobRetryUntil`
Defines timestamp retry deadline.
```php
public int $jobRetryUntil = 1610191764;
```
### `getJobRetryUntil`
Defines retry deadline as `DateTime`.
```php
public function getJobRetryUntil(): DateTime
{
return now()->addMinutes(30);
}
```
### `getJobDisplayName`
Customizes queued job display name.
```php
public function getJobDisplayName(): string
{
return 'Send team report email';
}
```
### `getJobTags`
Adds queue tags.
```php
public function getJobTags(Team $team): array
{
return ['report', 'team:'.$team->id];
}
```
### `getJobUniqueId`
Defines uniqueness key when using `ShouldBeUnique`.
```php
public function getJobUniqueId(Team $team): int
{
return $team->id;
}
```
### `$jobUniqueId`
Static uniqueness key alternative.
```php
public string $jobUniqueId = 'some_static_key';
```
### `getJobUniqueFor`
Defines uniqueness lock duration in seconds.
```php
public function getJobUniqueFor(Team $team): int
{
return $team->role === 'premium' ? 1800 : 3600;
}
```
### `$jobUniqueFor`
Property alternative for uniqueness lock duration.
```php
public int $jobUniqueFor = 3600;
```
### `getJobUniqueVia`
Defines cache driver used for uniqueness lock.
```php
public function getJobUniqueVia()
{
return Cache::driver('redis');
}
```
### `$jobDeleteWhenMissingModels`
Property alternative for missing model handling.
```php
public bool $jobDeleteWhenMissingModels = true;
```
### `getJobDeleteWhenMissingModels`
Defines whether jobs with missing models are deleted.
```php
public function getJobDeleteWhenMissingModels(): bool
{
return true;
}
```
### `jobFailed`
Handles job failure. Receives exception and dispatched parameters.
```php
public function jobFailed(?Throwable $e, ...$parameters): void
{
// Notify users, report errors, trigger compensations...
}
```
## Checklist
- Async/sync dispatch method matches use-case (`dispatch`, `dispatchSync`, `dispatchAfterResponse`).
- Queue config is explicit when needed (`$jobConnection`, `$jobQueue`, `configureJob`).
- Retry/backoff/timeout policies are intentional.
- `asJob(...)` delegates to `handle(...)` unless queue-specific branching is required.
- Queue tests use `Queue::fake()` and action assertions (`assertPushed*`).
## Common pitfalls
- Embedding domain logic only in `asJob(...)`.
- Forgetting uniqueness/timeout/retry controls on heavy jobs.
- Missing queue-specific assertions in tests.
## References
- https://www.laravelactions.com/2.x/as-job.html

View file

@ -0,0 +1,81 @@
# Listener Entrypoint (`asListener`)
## Scope
Use this reference when wiring actions to domain/application events.
## Recap
- Shows how listener execution maps event payloads into `handle(...)` arguments.
- Describes `asListener(...)` fallback behavior and adaptation role.
- Includes event registration example for provider wiring.
- Emphasizes test focus on dispatch and action interaction.
## Recommended pattern
- Register action listener in `EventServiceProvider` (or project equivalent).
- Use `asListener(Event $event)` for event adaptation.
- Delegate core logic to `handle(...)`.
## Methods used (`ListenerDecorator`)
### `asListener`
Called when executed as an event listener. If missing, it falls back to `handle(...)`.
```php
class SendOfferToNearbyDrivers
{
use AsAction;
public function handle(Address $source, Address $destination): void
{
// ...
}
public function asListener(TaxiRequested $event): void
{
$this->handle($event->source, $event->destination);
}
}
```
## Examples
### Event registration
```php
// app/Providers/EventServiceProvider.php
protected $listen = [
TaxiRequested::class => [
SendOfferToNearbyDrivers::class,
],
];
```
### Focused listener test
```php
use Illuminate\Support\Facades\Event;
Event::fake();
TaxiRequested::dispatch($source, $destination);
Event::assertDispatched(TaxiRequested::class);
```
## Checklist
- Event-to-listener mapping is registered.
- Listener method signature matches event contract.
- Listener tests verify dispatch and action interaction.
## Common pitfalls
- Assuming automatic listener registration when explicit mapping is required.
- Re-implementing business logic in `asListener(...)`.
## References
- https://www.laravelactions.com/2.x/as-listener.html

View file

@ -0,0 +1,118 @@
# Object Entrypoint (`run`, `make`, DI)
## Scope
Use this reference when the action is invoked as a plain object.
## Recap
- Explains object-style invocation with `make`, `run`, `runIf`, `runUnless`.
- Clarifies when to use static helpers versus DI/manual invocation.
- Includes minimal examples for direct run and service-level injection.
- Highlights boundaries: business logic stays in `handle(...)`.
## Recommended pattern
- Keep core business logic in `handle(...)`.
- Prefer `Action::run(...)` for readability.
- Use `Action::make()->handle(...)` or DI only when needed.
## Methods provided
### `make`
Resolves the action from the container.
```php
PublishArticle::make();
// Equivalent to:
app(PublishArticle::class);
```
### `run`
Resolves and executes the action.
```php
PublishArticle::run($articleId);
// Equivalent to:
PublishArticle::make()->handle($articleId);
```
### `runIf`
Resolves and executes the action only if the condition is met.
```php
PublishArticle::runIf($shouldPublish, $articleId);
// Equivalent mental model:
if ($shouldPublish) {
PublishArticle::run($articleId);
}
```
### `runUnless`
Resolves and executes the action only if the condition is not met.
```php
PublishArticle::runUnless($alreadyPublished, $articleId);
// Equivalent mental model:
if (! $alreadyPublished) {
PublishArticle::run($articleId);
}
```
## Checklist
- Input/output types are explicit.
- `handle(...)` has no transport concerns.
- Business behavior is covered by direct `handle(...)` tests.
## Common pitfalls
- Putting HTTP/CLI/queue concerns in `handle(...)`.
- Calling adapters from `handle(...)` instead of the reverse.
## References
- https://www.laravelactions.com/2.x/as-object.html
## Examples
### Minimal object-style invocation
```php
final class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
// Domain logic...
return true;
}
}
$published = PublishArticle::run(42);
```
### Dependency injection invocation
```php
final class ArticleService
{
public function __construct(
private PublishArticle $publishArticle
) {}
public function publish(int $articleId): bool
{
return $this->publishArticle->handle($articleId);
}
}
```

View file

@ -0,0 +1,160 @@
# Testing and Action Fakes
## Scope
Use this reference when isolating action orchestration in tests.
## Recap
- Summarizes all `AsFake` helpers (`mock`, `partialMock`, `spy`, `shouldRun`, `shouldNotRun`, `allowToRun`).
- Clarifies when to assert execution versus non-execution.
- Covers fake lifecycle checks/reset (`isFake`, `clearFake`).
- Provides branch-oriented test examples for orchestration confidence.
## Core methods
- `mock()`
- `partialMock()`
- `spy()`
- `shouldRun()`
- `shouldNotRun()`
- `allowToRun()`
- `isFake()`
- `clearFake()`
## Recommended pattern
- Test `handle(...)` directly for business rules.
- Test entrypoints for wiring/orchestration.
- Fake only at the boundary under test.
## Methods provided (`AsFake` trait)
### `mock`
Swaps the action with a full mock.
```php
FetchContactsFromGoogle::mock()
->shouldReceive('handle')
->with(42)
->andReturn(['Loris', 'Will', 'Barney']);
```
### `partialMock`
Swaps the action with a partial mock.
```php
FetchContactsFromGoogle::partialMock()
->shouldReceive('fetch')
->with('some_google_identifier')
->andReturn(['Loris', 'Will', 'Barney']);
```
### `spy`
Swaps the action with a spy.
```php
$spy = FetchContactsFromGoogle::spy()
->allows('handle')
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `shouldRun`
Helper adding expectation on `handle`.
```php
FetchContactsFromGoogle::shouldRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldReceive('handle');
```
### `shouldNotRun`
Helper adding negative expectation on `handle`.
```php
FetchContactsFromGoogle::shouldNotRun();
// Equivalent to:
FetchContactsFromGoogle::mock()->shouldNotReceive('handle');
```
### `allowToRun`
Helper allowing `handle` on a spy.
```php
$spy = FetchContactsFromGoogle::allowToRun()
->andReturn(['Loris', 'Will', 'Barney']);
// ...
$spy->shouldHaveReceived('handle')->with(42);
```
### `isFake`
Returns whether the action has been swapped with a fake.
```php
FetchContactsFromGoogle::isFake(); // false
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
```
### `clearFake`
Clears the fake instance, if any.
```php
FetchContactsFromGoogle::mock();
FetchContactsFromGoogle::isFake(); // true
FetchContactsFromGoogle::clearFake();
FetchContactsFromGoogle::isFake(); // false
```
## Examples
### Orchestration test
```php
it('runs sync contacts for premium teams', function () {
SyncGoogleContacts::shouldRun()->once()->with(42)->andReturnTrue();
ImportTeamContacts::run(42, isPremium: true);
});
```
### Guard-clause test
```php
it('does not run sync when integration is disabled', function () {
SyncGoogleContacts::shouldNotRun();
ImportTeamContacts::run(42, integrationEnabled: false);
});
```
## Checklist
- Assertions verify call intent and argument contracts.
- Fakes are cleared when leakage risk exists.
- Branch tests use `shouldRun()` / `shouldNotRun()` where clearer.
## Common pitfalls
- Over-mocking and losing behavior confidence.
- Asserting only dispatch, not business correctness.
## References
- https://www.laravelactions.com/2.x/as-fake.html

View file

@ -0,0 +1,33 @@
# Troubleshooting
## Scope
Use this reference when action wiring behaves unexpectedly.
## Recap
- Provides a fast triage flow for routing, queueing, events, and command wiring.
- Lists recurring failure patterns and where to check first.
- Encourages reproducing issues with focused tests before broad debugging.
- Separates wiring diagnostics from domain logic verification.
## Fast checks
- Action class uses `AsAction`.
- Namespace and autoloading are correct.
- Entrypoint wiring (route, queue, event, command) is registered.
- Method signatures and argument types match caller expectations.
## Failure patterns
- Controller route points to wrong class.
- Queue worker/config mismatch.
- Listener mapping not loaded.
- Command signature mismatch.
- Command not registered in the console kernel.
## Debug checklist
- Reproduce with a focused failing test.
- Validate wiring layer first, then domain behavior.
- Isolate dependencies with fakes/spies where appropriate.

View file

@ -0,0 +1,189 @@
# With Attributes (`WithAttributes` trait)
## Scope
Use this reference when an action stores and validates input via internal attributes instead of method arguments.
## Recap
- Documents attribute lifecycle APIs (`setRawAttributes`, `fill`, `fillFromRequest`, readers/writers).
- Clarifies behavior of key collisions (`fillFromRequest`: request data wins over route params).
- Lists validation/authorization hooks reused from controller validation pipeline.
- Includes end-to-end example from fill to `validateAttributes()` and `handle(...)`.
## Methods provided (`WithAttributes` trait)
### `setRawAttributes`
Replaces all attributes with the provided payload.
```php
$action->setRawAttributes([
'key' => 'value',
]);
```
### `fill`
Merges provided attributes into existing attributes.
```php
$action->fill([
'key' => 'value',
]);
```
### `fillFromRequest`
Merges request input and route parameters into attributes. Request input has priority over route parameters when keys collide.
```php
$action->fillFromRequest($request);
```
### `all`
Returns all attributes.
```php
$action->all();
```
### `only`
Returns attributes matching the provided keys.
```php
$action->only('title', 'body');
```
### `except`
Returns attributes excluding the provided keys.
```php
$action->except('body');
```
### `has`
Returns whether an attribute exists for the given key.
```php
$action->has('title');
```
### `get`
Returns the attribute value by key, with optional default.
```php
$action->get('title');
$action->get('title', 'Untitled');
```
### `set`
Sets an attribute value by key.
```php
$action->set('title', 'My blog post');
```
### `__get`
Accesses attributes as object properties.
```php
$action->title;
```
### `__set`
Updates attributes as object properties.
```php
$action->title = 'My blog post';
```
### `__isset`
Checks attribute existence as object properties.
```php
isset($action->title);
```
### `validateAttributes`
Runs authorization and validation using action attributes and returns validated data.
```php
$validatedData = $action->validateAttributes();
```
## Methods used (`AttributeValidator`)
`WithAttributes` uses the same authorization/validation hooks as `AsController`:
- `prepareForValidation`
- `authorize`
- `rules`
- `withValidator`
- `afterValidator`
- `getValidator`
- `getValidationData`
- `getValidationMessages`
- `getValidationAttributes`
- `getValidationRedirect`
- `getValidationErrorBag`
- `getValidationFailure`
- `getAuthorizationFailure`
## Example
```php
class CreateArticle
{
use AsAction;
use WithAttributes;
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:8'],
'body' => ['required', 'string'],
];
}
public function handle(array $attributes): Article
{
return Article::create($attributes);
}
}
$action = CreateArticle::make()->fill([
'title' => 'My first post',
'body' => 'Hello world',
]);
$validated = $action->validateAttributes();
$article = $action->handle($validated);
```
## Checklist
- Attribute keys are explicit and stable.
- Validation rules match expected attribute shape.
- `validateAttributes()` is called before side effects when needed.
- Validation/authorization hooks are tested in focused unit tests.
## Common pitfalls
- Mixing attribute-based and argument-based flows inconsistently in the same action.
- Assuming route params override request input in `fillFromRequest` (they do not).
- Skipping `validateAttributes()` when using external input.
## References
- https://www.laravelactions.com/2.x/with-attributes.html

View file

@ -0,0 +1,190 @@
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
author: laravel
---
# Laravel Best Practices
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
## Consistency First
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
## Quick Reference
### 1. Database Performance → `rules/db-performance.md`
- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
### 3. Security → `rules/security.md`
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
### 4. Caching → `rules/caching.md`
- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production
### 5. Eloquent Patterns → `rules/eloquent.md`
- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
### 6. Validation & Forms → `rules/validation.md`
- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`
### 7. Configuration → `rules/config.md`
- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text
### 8. Testing Patterns → `rules/testing.md`
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios
### 10. Routing & Controllers → `rules/routing.md`
- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation
### 11. HTTP Client → `rules/http-client.md`
- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails
### 13. Error Handling → `rules/error-handling.md`
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes
### 14. Task Scheduling → `rules/scheduling.md`
- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration
### 15. Architecture → `rules/architecture.md`
- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
### 16. Migrations → `rules/migrations.md`
- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML
### 17. Collections → `rules/collections.md`
- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections
### 18. Blade & Views → `rules/blade-views.md`
- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props
### 19. Conventions & Style → `rules/style.md`
- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files
## How to Apply
Always use a sub-agent to read rule files and explore this skill's content.
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version

View file

@ -0,0 +1,106 @@
# Advanced Query Patterns
## Use `addSelect()` Subqueries for Single Values from Has-Many
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
```php
public function scopeWithLastLoginAt($query): void
{
$query->addSelect([
'last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->withCasts(['last_login_at' => 'datetime']);
}
```
## Create Dynamic Relationships via Subquery FK
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
```php
public function lastLogin(): BelongsTo
{
return $this->belongsTo(Login::class);
}
public function scopeWithLastLogin($query): void
{
$query->addSelect([
'last_login_id' => Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
->take(1),
])->with('lastLogin');
}
```
## Use Conditional Aggregates Instead of Multiple Count Queries
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
```php
$statuses = Feature::toBase()
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
->first();
```
## Use `setRelation()` to Prevent Circular N+1
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
```php
$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);
```
## Prefer `whereIn` + Subquery Over `whereHas`
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
Incorrect (correlated EXISTS re-executes per row):
```php
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
```
Correct (index-friendly subquery, no PHP memory overhead):
```php
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
```
## Sometimes Two Simple Queries Beat One Complex Query
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
## Use Compound Indexes Matching `orderBy` Column Order
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
```php
// Migration
$table->index(['last_name', 'first_name']);
// Query — column order must match the index
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
```
## Use Correlated Subqueries for Has-Many Ordering
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
```php
public function scopeOrderByLastLogin($query): void
{
$query->orderByDesc(Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->take(1)
);
}
```

View file

@ -0,0 +1,202 @@
# Architecture Best Practices
## Single-Purpose Action Classes
Extract discrete business operations into invokable Action classes.
```php
class CreateOrderAction
{
public function __construct(private InventoryService $inventory) {}
public function execute(array $data): Order
{
$order = Order::create($data);
$this->inventory->reserve($order);
return $order;
}
}
```
## Use Dependency Injection
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
Incorrect:
```php
class OrderController extends Controller
{
public function store(StoreOrderRequest $request)
{
$service = app(OrderService::class);
return $service->create($request->validated());
}
}
```
Correct:
```php
class OrderController extends Controller
{
public function __construct(private OrderService $service) {}
public function store(StoreOrderRequest $request)
{
return $this->service->create($request->validated());
}
}
```
## Code to Interfaces
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
Incorrect (concrete dependency):
```php
class OrderService
{
public function __construct(private StripeGateway $gateway) {}
}
```
Correct (interface dependency):
```php
interface PaymentGateway
{
public function charge(int $amount, string $customerId): PaymentResult;
}
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
}
```
Bind in a service provider:
```php
$this->app->bind(PaymentGateway::class, StripeGateway::class);
```
## Default Sort by Descending
When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres.
Incorrect:
```php
$posts = Post::paginate();
```
Correct:
```php
$posts = Post::latest()->paginate();
```
## Use Atomic Locks for Race Conditions
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
```php
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
$order->process();
});
// Or at query level
$product = Product::where('id', $id)->lockForUpdate()->first();
```
## Use `mb_*` String Functions
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
Incorrect:
```php
strlen('José'); // 5 (bytes, not characters)
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
```
Correct:
```php
mb_strlen('José'); // 4 (characters)
mb_strtolower('MÜNCHEN'); // 'münchen'
// Prefer Laravel's Str helpers when available
Str::length('José'); // 4
Str::lower('MÜNCHEN'); // 'münchen'
```
## Use `defer()` for Post-Response Work
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
Incorrect (job overhead for trivial work):
```php
dispatch(new LogPageView($page));
```
Correct (runs after response, same process):
```php
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
```
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
## Use `Context` for Request-Scoped Data
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
```php
// In middleware
Context::add('tenant_id', $request->header('X-Tenant-ID'));
// Anywhere later — controllers, jobs, log context
$tenantId = Context::get('tenant_id');
```
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
## Use `Concurrency::run()` for Parallel Execution
Run independent operations in parallel using child processes — no async libraries needed.
```php
use Illuminate\Support\Facades\Concurrency;
[$users, $orders] = Concurrency::run([
fn () => User::count(),
fn () => Order::where('status', 'pending')->count(),
]);
```
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
## Convention Over Configuration
Follow Laravel conventions. Don't override defaults unnecessarily.
Incorrect:
```php
class Customer extends Model
{
protected $table = 'Customer';
protected $primaryKey = 'customer_id';
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
}
}
```
Correct:
```php
class Customer extends Model
{
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
```

View file

@ -0,0 +1,36 @@
# Blade & Views Best Practices
## Use `$attributes->merge()` in Component Templates
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
```blade
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
{{ $message }}
</div>
```
## Use `@pushOnce` for Per-Component Scripts
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
## Prefer Blade Components Over `@include`
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
## Use View Composers for Shared View Data
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
A single view can return either the full page or just a fragment, keeping routing clean.
```php
return view('dashboard', compact('users'))
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
```
## Use `@aware` for Deeply Nested Component Props
Avoids re-passing parent props through every level of nested components.

View file

@ -0,0 +1,70 @@
# Caching Best Practices
## Use `Cache::remember()` Instead of Manual Get/Put
Atomic pattern prevents race conditions and removes boilerplate.
Incorrect:
```php
$val = Cache::get('stats');
if (! $val) {
$val = $this->computeStats();
Cache::put('stats', $val, 60);
}
```
Correct:
```php
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
```
## Use `Cache::flexible()` for Stale-While-Revalidate
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
## Use Cache Tags to Invalidate Related Groups
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
```php
Cache::tags(['user-1'])->flush();
```
## Use `Cache::add()` for Atomic Conditional Writes
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
Correct: `Cache::add('lock', true, 10);`
## Use `once()` for Per-Request Memoization
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
```php
public function roles(): Collection
{
return once(fn () => $this->loadRoles());
}
```
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
## Configure Failover Cache Stores in Production
If Redis goes down, the app falls back to a secondary store automatically.
```php
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
```

Some files were not shown because too many files have changed in this diff Show more