Тема
Controllers, Routing, Middleware, Validation
Laravel 13 (PHP 8.3+) · сверено с laravel.com/docs/13.x Контекст «почему»: best/architecture-patterns (слои), best/security (периметр), best/laravel-13 (атрибуты, CSRF, JSON:API)
Роль контроллера
Контроллер — тонкая прослойка. Три действия:
- Принять и провалидировать input (FormRequest)
- Вызвать сервис
- Вернуть response (view / json / redirect)
Если в методе контроллера больше 10 строк логики — она должна быть в Service.
Routing
Route Model Binding
- По
slug— для публичных сущностей ({category:slug},{product:slug}) - По
id— только для внутренних/admin сущностей - Вложенные модели с
scopeBindings()— Laravel автоматически проверяет принадлежность child к parent
Группировка
- По
prefix— общий URL-префикс (/catalog/*,/api/*) - По
middleware— общие middleware (throttle,auth) - По
name— общий prefix для имён роутов (catalog.index,catalog.show) - Laravel 13: роуты с явным
domain()матчатся раньше роутов без домена — catch-all поддомены работают независимо от порядка регистрации
Именование
Каждый роут ДОЛЖЕН иметь имя через ->name(). Ссылки в коде — только через route('name'), никогда хардкод URL.
Редиректы
- 301 — permanent (для SEO, при переименовании URL)
- 302 — temporary (для временных перенаправлений)
- При любом изменении URL — обязателен 301 со старого адреса
Fallback
Определять fallback-роут для 404 — чтобы кастомная страница, а не дефолтная Laravel.
FormRequest (Validation)
- Отдельный FormRequest-класс для каждой формы/endpoint
- Валидация НИКОГДА не в контроллере — только в FormRequest
authorize()— true для публичных форм, проверка прав для защищённыхmessages()— кастомные сообщения на русском для пользовательских форм
Правила валидации
requiredvsnullable— осознанный выбор, не "на всякий случай"max:255на все string поля — защита от переполненияemail:rfc,dns— проверяет формат И существование доменаexists:table,column— для FK, проверка что связанная запись существуетunique:table,columnсignoreRecordпри update- Regex для телефонов и специфичных форматов
- Кастомные правила через
Rule::или invokable rule class
Middleware
Регистрация
В bootstrap/app.php через ->withMiddleware() (Kernel.php не существует с Laravel 11).
CSRF — PreventRequestForgery (Laravel 13)
- Middleware переименован:
VerifyCsrfToken→Illuminate\Foundation\Http\Middleware\PreventRequestForgery; добавлена проверка origin по заголовкуSec-Fetch-Site - Origin-проверка работает только по HTTPS (браузер шлёт заголовок только там); на HTTP — всегда fallback на CSRF-токен
- Конфигурация в
bootstrap/app.php:$middleware->preventRequestForgery(except: ['webhook/*']); опции —originOnly: true(без токена, 403 вместо 419),allowSameSite: true(разрешить поддомены) - В тестах отключать через
->withoutMiddleware([PreventRequestForgery::class])— старые имена deprecated
Атрибуты #[Middleware] / #[Authorize] (Laravel 13)
Middleware и авторизацию, специфичные для конкретного контроллера/метода, объявлять атрибутами на классе/методе:
php
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;
#[Middleware('auth')]
class CommentController
{
#[Middleware('subscribed')]
#[Authorize('create', [Comment::class, 'post'])]
public function store(Post $post)
{
// ...
}
}- Групповые middleware (throttle, auth на группу роутов) — по-прежнему в
routes/*.php - Не дублировать: одно правило живёт либо в роуте, либо в атрибуте
- Атрибут на методе сливается с атрибутом класса (merge, не replace) — учитывать при чтении цепочки
#[Authorize]заменяет$this->authorize()в теле метода — проверка объявлена до выполнения- Для
Route::resource()точечный middleware —->middlewareFor('show', 'auth')/->withoutMiddlewareFor(['create', 'store'], 'verified'), не разбивать resource на отдельные роуты
Типичные middleware
| Middleware | Назначение | Где применять |
|---|---|---|
throttle:X,Y | Rate limiting | API POST endpoints |
| Кастомный PageCache | Кеш GET-страниц в Redis | Публичные GET-роуты |
| Кастомный ResolveCity | Определение текущего города | Все публичные роуты |
auth | Авторизация | Admin-роуты |
verified | Подтверждение email | Защищённые действия |
Правила
- Middleware применять к группе роутов, не к каждому отдельно
- Middleware НЕ содержит бизнес-логику — только фильтрация/подготовка request
- Тяжёлые операции (DB-запросы) в middleware — минимизировать, кешировать
API Controllers
Коды ответов
| Код | Когда |
|---|---|
| 200 | GET успешный |
| 201 | POST создал ресурс |
| 204 | DELETE успешный, нет тела |
| 422 | Ошибка валидации (Laravel default) |
| 429 | Rate limit exceeded |
| 404 | Ресурс не найден |
| 500 | Серверная ошибка — НЕ возвращать детали в production |
Правила
- Возвращать JSON, не HTML
- Для сложных ответов — API Resources (трансформация модели в JSON)
- Rate limiting (
throttle) на каждый POST/PUT/DELETE endpoint - Не раскрывать внутренние ошибки (stack traces) в production
JSON:API Resources (Laravel 13)
- Публичный/партнёрский API по спецификации JSON:API — first-party ресурсы:
php artisan make:resource PostResource --json-api→ класс extendsIlluminate\Http\Resources\JsonApi\JsonApiResource - Из коробки: сериализация resource objects, relationships, sparse fieldsets, links, корректные JSON:API-заголовки
- Возврат:
$post->toResource()/Post::all()->toResourceCollection() - Relationships сериализуются только по
?include=— глубину ограничиватьJsonApiResource::maxRelationshipDepth(3)в провайдере; на include-путях проверять eager loading (N+1) - Парсинг входящих фильтров/сортировок —
spatie/laravel-query-builder(официальная рекомендация), не самописный разбор query string - Для простого внутреннего API достаточно обычного
JsonResource— JSON:API не тянуть «на всякий случай»
Anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
$request->validate() в контроллере | Валидация не в том месте | FormRequest |
| Больше 10 строк логики в методе контроллера | Fat controller | Service |
dd() / dump() в контроллере | Остаётся в коде | Логгер, Telescope |
Прямой DB:: в контроллере | Обход слоёв | Через модель/сервис |
| Хардкод URL в redirect | Ломается при смене роутов | route('name') |
Роут без ->name() | Невозможно ссылаться | Всегда именовать |
| Middleware с бизнес-логикой | Не то место | Логику в Service |
| Один контроллер на 20+ роутов | God controller | Разбить по доменам |