Laravel 13.x + PHP 8.3 rules for Cursor. Anti-Repository pattern stance (cites Otwell on Maintainable, Freek Van der Herten freek.dev/2422, Spatie boost-spatie-guidelines). Catches 37 LLM regressions including app/Http/Kernel.php (deleted in L11), app/Console/Kernel.php (deleted in L11), RouteServiceProvider regeneration, $casts array (use casts() method), scopeActive() (use #[Scope]), wire:model.lazy (Livewire 3 rename, default-deferred in Livewire 4), ShouldBeUnique inside Bus::batch(), Carbon 2 calls (Carbon 3 mandatory in L12+), image rule expecting SVG (use image:allow_svg), $guarded = [], five-default-providers register, Mix asset compilation (Vite is default), Pest 4 vs PHPUnit, Filament 3 resource shape, Octane defaulting Swoole when FrankenPHP is recommended.
# Laravel 11/12/13 Anti-Patterns
Cursor: when generating Laravel code, refuse to emit any of the patterns below. Each entry has the BAD form, the CORRECT replacement, and a one-line explanation of why the BAD form fails on Laravel 11, 12, or 13. Target: `laravel/framework ^13.0` on `php ^8.3`.
The companion `laravel-core` rule explains the L11 skeleton; `laravel-eloquent`, `laravel-controllers-and-actions`, `laravel-livewire-volt`, `laravel-inertia`, `laravel-queues-and-jobs`, `laravel-testing`, `laravel-security`, and `laravel-performance` cover the matching positive patterns.
## 1. Generates `app/Http/Kernel.php`
Deleted in Laravel 11. Middleware now configures via `bootstrap/app.php`.
```php
// BAD - app/Http/Kernel.php (file deleted in L11)
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel {
protected $middleware = [/* ... */];
protected $middlewareGroups = [/* ... */];
}
```
```php
// CORRECT - bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(web: __DIR__.'/../routes/web.php')
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [HandleInertiaRequests::class]);
$middleware->throttleApi();
})
->withExceptions(fn ($e) => $e)
->create();
```
## 2. Generates `app/Console/Kernel.php`
Deleted in Laravel 11. Console schedules and commands move to `routes/console.php` and `bootstrap/app.php`.
```php
// BAD - app/Console/Kernel.php (file deleted in L11)
class Kernel extends ConsoleKernel {
protected function schedule(Schedule $schedule): void {
$schedule->command('emails:send')->daily();
}
}
```
```php
// CORRECT - routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->daily();
```
## 3. Generates `app/Providers/RouteServiceProvider.php`
Deleted in Laravel 11. Routing is configured in `bootstrap/app.php` via `withRouting()`.
```php
// BAD - app/Providers/RouteServiceProvider.php
class RouteServiceProvider extends ServiceProvider {
public function boot(): void {
$this->routes(function () {
Route::middleware('api')->prefix('api')->group(base_path('routes/api.php'));
Route::middleware('web')->group(base_path('routes/web.php'));
});
}
}
```
```php
// CORRECT - bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->create();
```
## 4. Implements the Repository pattern over Eloquent
Taylor Otwell on the Maintainable podcast (2025, HN discussion: <https://news.ycombinator.com/item?id=45025685>) calls Repository-on-top-of-Eloquent the most common over-abstraction he sees. Freek Van der Herten (<https://freek.dev/2422-you-might-not-need-a-repository-in-laravel-3-alternatives>) and Spatie's house style (<https://github.com/spatie/boost-spatie-guidelines>) say the same: use the model directly, push complex business logic into Action classes or model methods.
```php
// BAD - app/Repositories/PostRepository.php
interface PostRepositoryInterface { public function find(int $id): ?Post; }
class EloquentPostRepository implements PostRepositoryInterface {
public function find(int $id): ?Post { return Post::find($id); }
}
class PostController { public function __construct(private PostRepositoryInterface $repo) {} }
```
```php
// CORRECT - call Eloquent directly; push logic into an Action
class PostController {
public function __invoke(int $id, PublishPost $publish): RedirectResponse {
$post = Post::findOrFail($id);
$publish($post);
return redirect()->route('posts.show', $post);
}
}
class PublishPost {
public function __invoke(Post $post): void {
$post->update(['published_at' => now()]);
event(new PostPublished($post));
}
}
```
## 5. Uses `protected $casts = [...]` property
L11+ ships a `casts()` method that runs after the parent boot, lets you reference enums and class constants safely, and is the documented form. The property still works but is the legacy shape.
```php
// BAD - app/Models/Post.php
class Post extends Model {
protected $casts = ['published_at' => 'datetime', 'meta' => 'array'];
}
```
```php
// CORRECT - app/Models/Post.php
class Post extends Model {
protected function casts(): array {
return [
'published_at' => 'datetime',
'meta' => AsArrayObject::class,
'status' => PostStatus::class,
];
}
}
```
## 6. Uses `scopeActive()` method instead of `#[Scope]` attribute
Laravel 12 introduced the `#[Scope]` attribute. The `scopeXxx` magic-method form still works but is no longer the recommended shape; the attribute is more discoverable and survives renames.
```php
// BAD - app/Models/Post.php
class Post extends Model {
public function scopeActive($query) { return $query->where('status', 'active'); }
public function scopePublished($query) { return $query->whereNotNull('published_at'); }
}
// Call site: Post::active()->published()->get(); // implicit magic
```
```php
// CORRECT - app/Models/Post.php
use Illuminate\Database\Eloquent\Attributes\Scope;
class Post extends Model {
#[Scope]
protected function active(Builder $query): void { $query->where('status', 'active'); }
#[Scope]
protected function published(Builder $query): void { $query->whereNotNull('published_at'); }
}
// Call site is identical: Post::active()->published()->get();
```
## 7. Uses `Cache::remember()` when `Cache::flexible()` fits
Laravel 11.23 added `Cache::flexible()` for stale-while-revalidate. It returns the stale value and refreshes in the background, eliminating the cache-miss latency spike `remember()` causes when the key expires under load.
```php
// BAD - reads block on the regeneration when the key expires
$posts = Cache::remember('homepage.posts', 3600, fn () => Post::published()->take(10)->get());
```
```php
// CORRECT - serves stale during regeneration, refreshes in background
$posts = Cache::flexible('homepage.posts', [60, 3600], fn () => Post::published()->take(10)->get());
// Fresh for 60s, stale-but-served-while-refreshing up to 3600s.
```
## 8. N+1 from missing `with()` / `withCount()` and reading relations in Blade
Loading a list and reading `$post->author->name` in the template fires one query per row. Eager-load the relation, or in 12.8+ enable automatic eager loading globally.
```php
// BAD - controller (no eager load)
$posts = Post::all();
```
```blade
{{-- BAD - template walks each row's relation, firing N queries --}}
@foreach ($posts as $post)
{{ $post->author->name }} - {{ $post->comments->count() }} comments
@endforeach
```
```php
// CORRECT - controller eager-loads up front
$posts = Post::with('author')->withCount('comments')->get();
// Or in AppServiceProvider::boot() for L12.8+:
// Model::automaticallyEagerLoadRelationships();
```
```blade
{{-- CORRECT - template reads the eager-loaded relation + the count column --}}
@foreach ($posts as $post)
{{ $post->author->name }} - {{ $post->comments_count }} comments
@endforeach
```
## 9. `$request->all()` straight into `Model::create()`
Mass-assignment vulnerability AND skips validation. `all()` returns every input the client sent, including ones the model accepts but the form did not expose (`is_admin`, `verified_at`).
```php
// BAD
public function store(Request $request) {
$post = Post::create($request->all());
return redirect()->route('posts.show', $post);
}
```
```php
// CORRECT
public function store(StorePostRequest $request) {
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
```
## 10. Inline `$request->validate([...])` for non-trivial forms
Inline `validate()` is fine for one or two fields. Anything with conditional rules, custom messages, or authorization belongs in a FormRequest.
```php
// BAD - 30 lines of inline validation in the controller
public function store(Request $request) {
$data = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'status' => 'required|in:draft,published',
'tags' => 'array',
'tags.*' => 'string|exists:tags,name',
// ... 15 more rules ...
]);
}
```
```php
// CORRECT - extract to a FormRequest with rules(), authorize(), messages()
public function store(StorePostRequest $request) {
$data = $request->validated();
}
```
## 11. Livewire 2 `wire:model.defer` / `wire:model.lazy`
Livewire 3 renamed both. `wire:model` is now default-deferred; opt back into live updates with `.live`. `wire:model.defer` and `wire:model.lazy` no longer mean what they did in v2.
```php
// BAD - Livewire 2 syntax
<input wire:model.defer="title">
<input wire:model.lazy="email"> <!-- v2: update on blur -->
```
```php
// CORRECT - Livewire 3+ (current, v4 unchanged)
<input wire:model="title"> <!-- defer is the default -->
<input wire:model.live="email"> <!-- update on every keystroke -->
<input wire:model.blur="email"> <!-- update on blur -->
<input wire:model.live.debounce.500ms="q"> <!-- live with debounce -->
```
## 12. PHPUnit `extends TestCase` when the project is Pest 3/4
New Laravel projects since L11 ship with Pest 4 by default. Mixing `class FooTest extends TestCase` files in a Pest project works but defeats `pest --parallel`, the browser-test runner, and architecture tests.
```php
// BAD - tests/Feature/PostTest.php
class PostTest extends TestCase {
use RefreshDatabase;
public function test_index_shows_posts(): void {
$this->get('/posts')->assertOk();
}
}
```
```php
// CORRECT - Pest 4 idiom
use function Pest\Laravel\get;
uses(RefreshDatabase::class);
it('shows posts', function () {
get('/posts')->assertOk();
});
test('index page contains the post title', function () {
Post::factory()->create(['title' => 'Hello']);
get('/posts')->assertSee('Hello');
});
```
## 13. `php artisan breeze:install` / `jetstream:install` in a fresh L13 project
Breeze and Jetstream were deprecated as the default starter kits in Laravel 12. The artisan install commands no longer apply to a fresh L13 project; new starter kits are selected at create time via `laravel new <name> --react|--vue|--livewire`.
```bash
# BAD
php artisan breeze:install
php artisan jetstream:install
```
```bash
# CORRECT (L12+) - select the starter kit when creating the project
laravel new myapp --livewire # Livewire 4 + Volt + Flux starter
```
## 14. Inertia v1 patterns (no `defer`, `WhenVisible`, `prefetch`, polling)
Inertia 3 introduced deferred props, `<Deferred>` / `<WhenVisible>` components, link prefetching, and polling helpers. Pages that load everything in the controller block on the slowest source.
```php
// BAD - controller blocks on slow analytics call
return Inertia::render('Dashboard', [
'user' => $request->user(),
'analytics' => $this->slowAnalytics(), // 2s
]);
```
```php
// CORRECT - defer the slow prop, load it after the page paints
return Inertia::render('Dashboard', [
'user' => $request->user(),
'analytics' => Inertia::defer(fn () => $this->slowAnalytics()),
]);
```
```jsx
// CORRECT - resources/js/Pages/Dashboard.jsx
import { Deferred, WhenVisible } from '@inertiajs/react';
<Deferred data="analytics" fallback={<Skeleton />}>
<AnalyticsPanel />
</Deferred>
<WhenVisible data="comments" fallback={<Spinner />}>
<Comments />
</WhenVisible>
```
## 15. `signed` middleware without `validateSignatures(except: [...])`
Marketing tools tack on `utm_source`, `utm_medium`, `fbclid`, `gclid` as the user clicks a signed URL. Those tracking params are NOT in the original signature, so signature verification fails and the link 403s.
```php
// BAD - bootstrap/app.php
$middleware->throttleApi();
// No validateSignatures customisation; any UTM param breaks signed URLs.
```
```php
// CORRECT - bootstrap/app.php
$middleware->validateSignatures(except: [
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'fbclid', 'gclid', 'msclkid', 'mc_cid', 'mc_eid', 'ref',
]);
```
## 16. `ShouldBeUnique` inside `Bus::batch()` or `Bus::chain()`
`ShouldBeUnique` works on individual queue dispatch. Inside a batch or chain Laravel dispatches the inner jobs without honouring the unique lock; you get duplicate execution.
```php
// BAD - the unique lock is silently ignored inside a batch
class GenerateInvoice implements ShouldQueue, ShouldBeUnique {
public int $uniqueFor = 600;
public function uniqueId(): string { return $this->order->id; }
}
Bus::batch([new GenerateInvoice($order), new GenerateInvoice($order)])->dispatch();
```
```php
// CORRECT - use WithoutOverlapping middleware (works inside batches/chains)
class GenerateInvoice implements ShouldQueue {
public function middleware(): array {
return [(new WithoutOverlapping($this->order->id))->expireAfter(600)];
}
}
```
## 17. Carbon 2 method calls (broken in L12+)
Laravel 12 raised `nesbot/carbon` to `^3.0`. Carbon 3 dropped or renamed several APIs (e.g. `diffInRealHours`, magic `getDays`, deprecated locale shapes). Code written against Carbon 2 throws at runtime.
```php
// BAD - Carbon 2 only
$hours = $end->diffInRealHours($start); // method removed in C3
$post->created_at->subDays(7)->toIso8601ZuluString(); // OK
$now->setTimezone(false); // C2 silent, C3 throws TypeError
```
```php
// CORRECT - Carbon 3
$hours = $end->diffInHours($start, false); // false keeps the sign
$post->created_at->subDays(7)->toIso8601ZuluString();
$now->setTimezone('UTC'); // pass a real timezone
```
## 18. `image` validation rule expecting SVG support
Laravel 12+ removed SVG from the default `image` rule because the previous behaviour exposed XSS via SVG. Opt in explicitly with `image:allow_svg`.
```php
// BAD - L12+ silently rejects SVG uploads even though the rule looks permissive
'avatar' => ['required', 'image', 'max:2048'],
```
```php
// CORRECT - if you actually want SVG (and have sanitised it), opt in
'avatar' => ['required', 'image:allow_svg', 'max:2048'],
// Otherwise leave 'image' alone and document that SVG is rejected by design.
```
## 19. Manual eager loading everywhere when automatic eager loading fits
Laravel 12.8 added `Model::automaticallyEagerLoadRelationships()`. Enable it in dev (and in prod when you have measured the safety) and drop the boilerplate `with()` chains from list endpoints.
```php
// BAD - dozens of with() chains scattered across the app
$posts = Post::with(['author', 'tags', 'comments.user', 'image'])->get();
$users = User::with(['posts.tags', 'posts.author'])->get();
```
```php
// CORRECT - app/Providers/AppServiceProvider.php boot()
public function boot(): void {
Model::shouldBeStrict(! app()->isProduction());
Model::automaticallyEagerLoadRelationships(); // L12.8+
}
// Then call sites become: Post::all() and the framework eager-loads on access.
```
## 20. `protected $guarded = []` (open mass-assignment)
`$guarded = []` disables mass-assignment protection entirely. Combined with `$request->all()` (see #9) the user can write any column. Always declare an explicit `$fillable` whitelist.
```php
// BAD - app/Models/Post.php
class Post extends Model {
protected $guarded = [];
}
// Pair with $request->all() and the user can set is_admin, role_id, balance, ...
```
```php
// CORRECT - app/Models/Post.php
class Post extends Model {
protected $fillable = ['title', 'body', 'status', 'published_at'];
}
// Pair with FormRequest::validated() and only safe fields reach create().
```
## 21. Generates removed config files (`broadcasting`, `cors`, `hashing`, `sanctum`, `view`)
L11 removed several config files because their defaults are good and they were almost never customised. Regenerating them adds noise and drifts out of sync with framework defaults on minor releases.
```bash
# BAD - copying a v10 install
config/broadcasting.php
config/cors.php
config/hashing.php
config/sanctum.php
config/view.php
```
```bash
# CORRECT - only publish a config file when you need to customise it
php artisan config:publish broadcasting # only if you actually need to
php artisan config:publish cors # only for non-default origins
# By default, ship with whatever config/ files you genuinely changed.
```
## 22. Five default service providers in `config/app.php`
L11 collapsed the five default providers (App, Auth, Broadcast, Event, Route) into one: `AppServiceProvider`. New providers register in `bootstrap/providers.php`, not `config/app.php`.
```php
// BAD - config/app.php (L10 shape)
'providers' => [
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
```
```php
// CORRECT - bootstrap/providers.php (L11+)
return [
App\Providers\AppServiceProvider::class,
// Add new providers here. Auth/Broadcast/Event/Route logic lives in AppServiceProvider.
];
```
## 23. Assumes `routes/api.php` and `routes/channels.php` are enabled
In L11+ both files are opt-in. Run `php artisan install:api` to add `routes/api.php` and Sanctum scaffolding, and `install:broadcasting` to add `routes/channels.php` and Reverb. Code that references `Route::group(api)` or `Broadcast::channel(...)` without those installs silently has no routes.
```bash
# BAD - assumes the file exists
# routes/api.php and routes/channels.php just present in a fresh install
```
```bash
# CORRECT - opt in explicitly
php artisan install:api # adds routes/api.php + Sanctum
php artisan install:broadcasting # adds routes/channels.php + Reverb
```
## 24. Pins PHP 8.1 or 8.2 in `composer.json` for L13
Laravel 13 requires `php: ^8.3`. Pinning 8.1/8.2 either silently keeps the user on L12, or breaks `composer update`.
```json
// BAD - composer.json
{
"require": {
"php": "^8.2",
"laravel/framework": "^13.0"
}
}
```
```json
// CORRECT - composer.json
{
"require": {
"php": "^8.3",
"laravel/framework": "^13.0"
}
}
```
## 25. Volt installation step for new Livewire 4 single-file components
Livewire 4 absorbed the single-file Blade component shape into the core. Volt is now optional, kept for the functional API people who already use it. Telling the LLM to run `php artisan volt:install` for a brand-new L13 project adds a dependency the user does not need.
```bash
# BAD - unnecessary install for a fresh L13 + Livewire 4 project
composer require livewire/volt
php artisan volt:install
```
```bash
# CORRECT - Livewire 4 SFC works without Volt
php artisan make:livewire Counter --inline
# resources/views/livewire/counter.blade.php has the class + view in one file natively
```
If you used `laravel new --livewire`, Volt is already installed and that is fine. The anti-pattern is reaching for `composer require livewire/volt` in a project that does not use the functional API.
## 26. Reverb described as "beta"
Reverb is GA since 1.0 and is the default WebSocket broadcaster shipped with `php artisan install:broadcasting`. Telling the user it is beta or "experimental" misrepresents the install path.
```php
// BAD copy ("Reverb is beta, use Pusher in production")
// CORRECT copy: Reverb is GA. Default broadcaster, runs locally with php artisan reverb:start
```
## 27. Defaults Octane to Swoole or RoadRunner
FrankenPHP became the recommended Octane runtime in 2025. Swoole and RoadRunner still work but are no longer the documented default; FrankenPHP ships HTTP/2, HTTP/3, early hints, and Mercure out of the box.
```bash
# BAD
php artisan octane:install --server=swoole
php artisan octane:install --server=roadrunner
```
```bash
# CORRECT
php artisan octane:install --server=frankenphp
php artisan octane:start --server=frankenphp
```
## 28. Pulse described as "beta"
Pulse is GA and ships with the new Livewire and Inertia starter kits. It is the recommended app monitor (CPU, slow queries, slow jobs, exceptions, slow requests).
```bash
# BAD copy ("Pulse is beta")
# CORRECT - install + ship in production
composer require laravel/pulse
php artisan vendor:publish --tag=pulse-migrations
php artisan migrate
# Visit /pulse (gate via PulseServiceProvider in production).
```
## 29. `laravel new --jet` / `laravel new --breeze` flags
Both flags were removed from the `laravel new` command in Laravel 12. The starter-kit selection now happens via `--react`, `--vue`, `--livewire`, or interactively. The flags themselves no longer exist; passing them errors at the CLI.
```bash
# BAD - removed CLI flags
laravel new myapp --jet
laravel new myapp --breeze --pest
```
```bash
# CORRECT
laravel new myapp --livewire --pest
laravel new myapp --react
laravel new myapp # interactive prompt
```
## 30. Old Filament 3 resource structure
Filament 4 replaced the `form()` / `table()` / `infolist()` static array shape with first-class Schema components. The v3 shape still works briefly under v4 with a compat layer, but new resources should use Schemas.
```php
// BAD - Filament 3
public static function form(Form $form): Form {
return $form->schema([
TextInput::make('title')->required(),
Textarea::make('body'),
]);
}
```
```php
// CORRECT - Filament 4
use Filament\Schemas\Schema;
public static function form(Schema $schema): Schema {
return $schema->components([
TextInput::make('title')->required(),
Textarea::make('body'),
]);
}
```
## 31. Trix editor in Filament
Filament 4 dropped the Trix component because of accessibility issues and replaced it with TipTap-based RichEditor.
```php
// BAD - Filament 3 patterns. Filament 4 swapped the editor backend from Trix to TipTap;
// configuration that targeted Trix specifically no longer applies.
RichEditor::make('body')
->disableToolbarButtons(['attachFiles', 'codeBlock']);
```
```php
// CORRECT - Filament 4
use Filament\Forms\Components\RichEditor;
RichEditor::make('body')->toolbarButtons(['bold', 'italic', 'link', 'bulletList']);
```
## 32. Laravel Mix asset compilation
Vite has been the default since Laravel 9.19 and the only documented option since L10. New code referencing `webpack.mix.js`, `mix()` helper, or `npm run dev` mapping to Mix is wrong.
```js
// BAD - webpack.mix.js + mix('css/app.css') in Blade
mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [require('tailwindcss')]);
```
```php
{{-- BAD - resources/views/layouts/app.blade.php --}}
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
```
```js
// CORRECT - vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true })],
});
```
```php
{{-- CORRECT - layout --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
```
## 33. `preventLazyLoading` missing in dev
Without it, an N+1 introduced by a junior PR ships silently. With it, lazy-loading throws in dev so the test suite catches the regression. Production should still allow lazy-loading (do not surprise paying users).
```php
// BAD - app/Providers/AppServiceProvider.php
public function boot(): void {
// strict mode never enabled; N+1 sneaks in unobserved
}
```
```php
// CORRECT - app/Providers/AppServiceProvider.php
public function boot(): void {
Model::shouldBeStrict(! app()->isProduction());
// shouldBeStrict turns on preventLazyLoading + preventSilentlyDiscardingAttributes + preventAccessingMissingAttributes
}
```
## 34. Service providers manually registered in `config/app.php`
Since L11 the canonical place to register providers is `bootstrap/providers.php`. Adding to `config/app.php` providers array still works but is the legacy path; `package:discover` writes to `bootstrap/providers.php`.
```php
// BAD - config/app.php
'providers' => ServiceProvider::defaultProviders()->merge([
App\Providers\TelescopeServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
])->toArray(),
```
```php
// CORRECT - bootstrap/providers.php
return [
App\Providers\AppServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
];
```
## 35. Uses `Hash::check` in user-supplied loops without rate limiting
Login and password-reset endpoints called repeatedly are an attacker's preferred CPU sink (bcrypt is intentionally slow). Wrap with `RateLimiter::for(...)` or use the throttle middleware on the route.
```php
// BAD - bare login endpoint
Route::post('/login', LoginController::class);
```
```php
// CORRECT - throttled route
Route::post('/login', LoginController::class)->middleware('throttle:5,1');
// Or in bootstrap/app.php:
// $middleware->throttleApi(); // 60/min per token, 1000/day per token
```
## 36. UUID columns generated as v4 when v7 is now the default for ordering
Laravel 11.20+ ships UUIDv7 helpers (`Str::uuid7()`, the `HasUuids` trait emits v7 by default in L11.20+). v7 is time-ordered, so it indexes well as a primary key; v4 is random and shreds the b-tree.
```php
// BAD - v4 random UUIDs as primary key (poor index locality)
class Post extends Model {
use HasUuids;
public function newUniqueId(): string { return (string) Str::uuid(); } // v4
}
```
```php
// CORRECT - v7 time-ordered UUIDs
class Post extends Model {
use HasUuids;
// Laravel 11.20+ HasUuids trait already emits v7 by default.
// For explicit control:
public function newUniqueId(): string { return (string) Str::uuid7(); }
}
```
## 37. Custom health check route over the built-in `/up`
L11 ships a built-in health route at `/up` configured in `bootstrap/app.php` via `withRouting(health: '/up')`. The endpoint emits the `Illuminate\Foundation\Events\DiagnosingHealth` event so you can attach custom checks.
```php
// BAD - rolling your own
Route::get('/healthz', fn () => response()->json(['ok' => true]));
```
```php
// CORRECT - bootstrap/app.php
->withRouting(
web: __DIR__.'/../routes/web.php',
health: '/up',
)
// Add custom checks via:
// Event::listen(DiagnosingHealth::class, fn ($e) => DB::connection()->getPdo());
```
## How to use this rule
When writing or reviewing PHP, refuse to emit any of the BAD forms above. When the user asks for a Repository class, push back with the citations from #4 and offer the Action / model-method alternative. When a model lands without `casts()`, `#[Scope]`, or explicit `$fillable`, fix it before proceeding. The companion `laravel-reviewer` agent groups these by severity for review-time use.