Skip to content

Commit 733347c

Browse files
committed
Laravel/boost upgrade
1 parent a067bcf commit 733347c

92 files changed

Lines changed: 9002 additions & 287 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
name: laravel-best-practices
3+
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."
4+
license: MIT
5+
metadata:
6+
author: laravel
7+
---
8+
9+
# Laravel Best Practices
10+
11+
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
12+
13+
## Consistency First
14+
15+
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.
16+
17+
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.
18+
19+
## Quick Reference
20+
21+
### 1. Database Performance → `rules/db-performance.md`
22+
23+
- Eager load with `with()` to prevent N+1 queries
24+
- Enable `Model::preventLazyLoading()` in development
25+
- Select only needed columns, avoid `SELECT *`
26+
- `chunk()` / `chunkById()` for large datasets
27+
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
28+
- `withCount()` instead of loading relations to count
29+
- `cursor()` for memory-efficient read-only iteration
30+
- Never query in Blade templates
31+
32+
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
33+
34+
- `addSelect()` subqueries over eager-loading entire has-many for a single value
35+
- Dynamic relationships via subquery FK + `belongsTo`
36+
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
37+
- `setRelation()` to prevent circular N+1 queries
38+
- `whereIn` + `pluck()` over `whereHas` for better index usage
39+
- Two simple queries can beat one complex query
40+
- Compound indexes matching `orderBy` column order
41+
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
42+
43+
### 3. Security → `rules/security.md`
44+
45+
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
46+
- No raw SQL with user input — use Eloquent or query builder
47+
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
48+
- Validate MIME type, extension, and size for file uploads
49+
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
50+
51+
### 4. Caching → `rules/caching.md`
52+
53+
- `Cache::remember()` over manual get/put
54+
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
55+
- `Cache::memo()` to avoid redundant cache hits within a request
56+
- Cache tags to invalidate related groups
57+
- `Cache::add()` for atomic conditional writes
58+
- `once()` to memoize per-request or per-object lifetime
59+
- `Cache::lock()` / `lockForUpdate()` for race conditions
60+
- Failover cache stores in production
61+
62+
### 5. Eloquent Patterns → `rules/eloquent.md`
63+
64+
- Correct relationship types with return type hints
65+
- Local scopes for reusable query constraints
66+
- Global scopes sparingly — document their existence
67+
- Attribute casts in the `casts()` method
68+
- Cast date columns, use Carbon instances in templates
69+
- `whereBelongsTo($model)` for cleaner queries
70+
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
71+
72+
### 6. Validation & Forms → `rules/validation.md`
73+
74+
- Form Request classes, not inline validation
75+
- Array notation `['required', 'email']` for new code; follow existing convention
76+
- `$request->validated()` only — never `$request->all()`
77+
- `Rule::when()` for conditional validation
78+
- `after()` instead of `withValidator()`
79+
80+
### 7. Configuration → `rules/config.md`
81+
82+
- `env()` only inside config files
83+
- `App::environment()` or `app()->isProduction()`
84+
- Config, lang files, and constants over hardcoded text
85+
86+
### 8. Testing Patterns → `rules/testing.md`
87+
88+
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
89+
- `assertModelExists()` over raw `assertDatabaseHas()`
90+
- Factory states and sequences over manual overrides
91+
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
92+
- `recycle()` to share relationship instances across factories
93+
94+
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
95+
96+
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
97+
- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency
98+
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
99+
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
100+
- Horizon for complex multi-queue scenarios
101+
102+
### 10. Routing & Controllers → `rules/routing.md`
103+
104+
- Implicit route model binding
105+
- Scoped bindings for nested resources
106+
- `Route::resource()` or `apiResource()`
107+
- Methods under 10 lines — extract to actions/services
108+
- Type-hint Form Requests for auto-validation
109+
110+
### 11. HTTP Client → `rules/http-client.md`
111+
112+
- Explicit `timeout` and `connectTimeout` on every request
113+
- `retry()` with exponential backoff for external APIs
114+
- Check response status or use `throw()`
115+
- `Http::pool()` for concurrent independent requests
116+
- `Http::fake()` and `preventStrayRequests()` in tests
117+
118+
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
119+
120+
- Event discovery over manual registration; `event:cache` in production
121+
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
122+
- Queue notifications and mailables with `ShouldQueue`
123+
- On-demand notifications for non-user recipients
124+
- `HasLocalePreference` on notifiable models
125+
- `assertQueued()` not `assertSent()` for queued mailables
126+
- Markdown mailables for transactional emails
127+
128+
### 13. Error Handling → `rules/error-handling.md`
129+
130+
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
131+
- `ShouldntReport` for exceptions that should never log
132+
- Throttle high-volume exceptions to protect log sinks
133+
- `dontReportDuplicates()` for multi-catch scenarios
134+
- Force JSON rendering for API routes
135+
- Structured context via `context()` on exception classes
136+
137+
### 14. Task Scheduling → `rules/scheduling.md`
138+
139+
- `withoutOverlapping()` on variable-duration tasks
140+
- `onOneServer()` on multi-server deployments
141+
- `runInBackground()` for concurrent long tasks
142+
- `environments()` to restrict to appropriate environments
143+
- `takeUntilTimeout()` for time-bounded processing
144+
- Schedule groups for shared configuration
145+
146+
### 15. Architecture → `rules/architecture.md`
147+
148+
- Single-purpose Action classes; dependency injection over `app()` helper
149+
- Prefer official Laravel packages and follow conventions, don't override defaults
150+
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
151+
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
152+
153+
### 16. Migrations → `rules/migrations.md`
154+
155+
- Generate migrations with `php artisan make:migration`
156+
- `constrained()` for foreign keys
157+
- Never modify migrations that have run in production
158+
- Add indexes in the migration, not as an afterthought
159+
- Mirror column defaults in model `$attributes`
160+
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
161+
- One concern per migration — never mix DDL and DML
162+
163+
### 17. Collections → `rules/collections.md`
164+
165+
- Higher-order messages for simple collection operations
166+
- `cursor()` vs. `lazy()` — choose based on relationship needs
167+
- `lazyById()` when updating records while iterating
168+
- `toQuery()` for bulk operations on collections
169+
170+
### 18. Blade & Views → `rules/blade-views.md`
171+
172+
- `$attributes->merge()` in component templates
173+
- Blade components over `@include`; `@pushOnce` for per-component scripts
174+
- View Composers for shared view data
175+
- `@aware` for deeply nested component props
176+
177+
### 19. Conventions & Style → `rules/style.md`
178+
179+
- Follow Laravel naming conventions for all entities
180+
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
181+
- No JS/CSS in Blade, no HTML in PHP classes
182+
- Code should be readable; comments only for config files
183+
184+
## How to Apply
185+
186+
Always use a sub-agent to read rule files and explore this skill's content.
187+
188+
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
189+
2. Check sibling files for existing patterns — follow those first per Consistency First
190+
3. Verify API syntax with `search-docs` for the installed Laravel version
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Advanced Query Patterns
2+
3+
## Use `addSelect()` Subqueries for Single Values from Has-Many
4+
5+
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.
6+
7+
```php
8+
public function scopeWithLastLoginAt($query): void
9+
{
10+
$query->addSelect([
11+
'last_login_at' => Login::select('created_at')
12+
->whereColumn('user_id', 'users.id')
13+
->latest()
14+
->take(1),
15+
])->withCasts(['last_login_at' => 'datetime']);
16+
}
17+
```
18+
19+
## Create Dynamic Relationships via Subquery FK
20+
21+
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.
22+
23+
```php
24+
public function lastLogin(): BelongsTo
25+
{
26+
return $this->belongsTo(Login::class);
27+
}
28+
29+
public function scopeWithLastLogin($query): void
30+
{
31+
$query->addSelect([
32+
'last_login_id' => Login::select('id')
33+
->whereColumn('user_id', 'users.id')
34+
->latest()
35+
->take(1),
36+
])->with('lastLogin');
37+
}
38+
```
39+
40+
## Use Conditional Aggregates Instead of Multiple Count Queries
41+
42+
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.
43+
44+
```php
45+
$statuses = Feature::toBase()
46+
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
47+
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
48+
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
49+
->first();
50+
```
51+
52+
## Use `setRelation()` to Prevent Circular N+1
53+
54+
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.
55+
56+
```php
57+
$feature->load('comments.user');
58+
$feature->comments->each->setRelation('feature', $feature);
59+
```
60+
61+
## Prefer `whereIn` + Subquery Over `whereHas`
62+
63+
`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.
64+
65+
Incorrect (correlated EXISTS re-executes per row):
66+
67+
```php
68+
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
69+
```
70+
71+
Correct (index-friendly subquery, no PHP memory overhead):
72+
73+
```php
74+
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
75+
```
76+
77+
## Sometimes Two Simple Queries Beat One Complex Query
78+
79+
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.
80+
81+
## Use Compound Indexes Matching `orderBy` Column Order
82+
83+
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.
84+
85+
```php
86+
// Migration
87+
$table->index(['last_name', 'first_name']);
88+
89+
// Query — column order must match the index
90+
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
91+
```
92+
93+
## Use Correlated Subqueries for Has-Many Ordering
94+
95+
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.
96+
97+
```php
98+
public function scopeOrderByLastLogin($query): void
99+
{
100+
$query->orderByDesc(Login::select('created_at')
101+
->whereColumn('user_id', 'users.id')
102+
->latest()
103+
->take(1)
104+
);
105+
}
106+
```

0 commit comments

Comments
 (0)