Skip to content

Commit 532cdb3

Browse files
init
1 parent d95c48c commit 532cdb3

118 files changed

Lines changed: 19468 additions & 0 deletions

File tree

Some content is hidden

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

.cursor/mcp.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"mcpServers": {
3+
"laravel-boost": {
4+
"command": "php",
5+
"args": [
6+
"artisan",
7+
"boost:mcp"
8+
]
9+
}
10+
}
11+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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, organized as an index of rule files. Each rule file 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, and 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+
## How to Apply
20+
21+
1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out.
22+
2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files.
23+
3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job.
24+
4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable.
25+
5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them.
26+
6. Re-read the diff against every mapped rule before finishing.
27+
28+
## Rule Index
29+
30+
Cross-cutting changes often need more than one rule file.
31+
32+
| Concern | Read |
33+
| --- | --- |
34+
| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) |
35+
| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) |
36+
| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) |
37+
| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) |
38+
| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) |
39+
| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) |
40+
| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) |
41+
| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) |
42+
| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) |
43+
| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) |
44+
| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) |
45+
| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) |
46+
| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) |
47+
| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) |
48+
| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
49+
| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
50+
| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
51+
| Pest/PHPUnit patterns, factories, fakes | [`rules/testing.md`](rules/testing.md) |
52+
| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
53+
| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |
54+
55+
## Decision Rules
56+
57+
- Prefer framework features and existing application abstractions over new helpers or dependencies.
58+
- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable.
59+
- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization.
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+
```
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Architecture Best Practices
2+
3+
## Single-Purpose Action Classes
4+
5+
Extract discrete business operations into invokable Action classes.
6+
7+
```php
8+
class CreateOrderAction
9+
{
10+
public function __construct(private InventoryService $inventory) {}
11+
12+
public function handle(array $data): Order
13+
{
14+
$order = Order::create($data);
15+
$this->inventory->reserve($order);
16+
17+
return $order;
18+
}
19+
}
20+
```
21+
22+
## Use Dependency Injection
23+
24+
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
25+
26+
Incorrect:
27+
```php
28+
class OrderController extends Controller
29+
{
30+
public function store(StoreOrderRequest $request)
31+
{
32+
$service = app(OrderService::class);
33+
34+
return $service->create($request->validated());
35+
}
36+
}
37+
```
38+
39+
Correct:
40+
```php
41+
class OrderController extends Controller
42+
{
43+
public function __construct(private OrderService $service) {}
44+
45+
public function store(StoreOrderRequest $request)
46+
{
47+
return $this->service->create($request->validated());
48+
}
49+
}
50+
```
51+
52+
## Code to Interfaces
53+
54+
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
55+
56+
Incorrect (concrete dependency):
57+
```php
58+
class OrderService
59+
{
60+
public function __construct(private StripeGateway $gateway) {}
61+
}
62+
```
63+
64+
Correct (interface dependency):
65+
```php
66+
interface PaymentGateway
67+
{
68+
public function charge(int $amount, string $customerId): PaymentResult;
69+
}
70+
71+
class OrderService
72+
{
73+
public function __construct(private PaymentGateway $gateway) {}
74+
}
75+
```
76+
77+
Bind in a service provider:
78+
79+
```php
80+
$this->app->bind(PaymentGateway::class, StripeGateway::class);
81+
```
82+
83+
## Default Sort by Descending
84+
85+
When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
86+
87+
Incorrect:
88+
```php
89+
$posts = Post::paginate();
90+
```
91+
92+
Correct:
93+
```php
94+
$posts = Post::latest()->paginate();
95+
```
96+
97+
## Use Atomic Locks for Race Conditions
98+
99+
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
100+
101+
```php
102+
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
103+
$order->process();
104+
});
105+
106+
// Or at query level
107+
$product = Product::where('id', $id)->lockForUpdate()->first();
108+
```
109+
110+
## Use `mb_*` String Functions
111+
112+
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
113+
114+
Incorrect:
115+
```php
116+
strlen('José'); // 5 (bytes, not characters)
117+
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
118+
```
119+
120+
Correct:
121+
```php
122+
mb_strlen('José'); // 4 (characters)
123+
mb_strtolower('MÜNCHEN'); // 'münchen'
124+
125+
// Prefer Laravel's Str helpers when available
126+
Str::length('José'); // 4
127+
Str::lower('MÜNCHEN'); // 'münchen'
128+
```
129+
130+
## Use `defer()` for Post-Response Work
131+
132+
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.
133+
134+
Incorrect (job overhead for trivial work):
135+
```php
136+
dispatch(new LogPageView($page));
137+
```
138+
139+
Correct (runs after response, same process):
140+
```php
141+
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
142+
```
143+
144+
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
145+
146+
## Use `Context` for Request-Scoped Data
147+
148+
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
149+
150+
```php
151+
// In middleware
152+
Context::add('tenant_id', $request->header('X-Tenant-ID'));
153+
154+
// Anywhere later — controllers, jobs, log context
155+
$tenantId = Context::get('tenant_id');
156+
```
157+
158+
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`.
159+
160+
## Use `Concurrency::run()` for Parallel Execution
161+
162+
Run independent operations in parallel using child processes — no async libraries needed.
163+
164+
```php
165+
use Illuminate\Support\Facades\Concurrency;
166+
167+
[$users, $orders] = Concurrency::run([
168+
fn () => User::count(),
169+
fn () => Order::where('status', 'pending')->count(),
170+
]);
171+
```
172+
173+
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.
174+
175+
## Convention Over Configuration
176+
177+
Follow Laravel conventions. Don't override defaults unnecessarily.
178+
179+
Incorrect:
180+
```php
181+
class Customer extends Model
182+
{
183+
protected $table = 'Customer';
184+
protected $primaryKey = 'customer_id';
185+
186+
public function roles(): BelongsToMany
187+
{
188+
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
189+
}
190+
}
191+
```
192+
193+
Correct:
194+
```php
195+
class Customer extends Model
196+
{
197+
public function roles(): BelongsToMany
198+
{
199+
return $this->belongsToMany(Role::class);
200+
}
201+
}
202+
```

0 commit comments

Comments
 (0)