Маршруты и контроллеры в Laravel: разбор на живом примере
Разбираем на реальном CRUD-модуле, как устроена связка маршрут-контроллер в Laravel 11: где чаще всего ломается binding, как route model injection экономит запросы и когда resource-маршруты выходят боком.

Возьмём один рабочий модуль — управление статьями блога — и разберём, как в Laravel 11.x запрос проходит путь от строки в routes/web.php до метода контроллера и обратно. Не абстрактный hello world, а то, обо что спотыкаются на живых проектах: почему параметр не подставился, откуда лишние запросы к базе, когда Route::resource помогает, а когда прячет проблему.
Очевидное решение — писать по замыканию на каждый URL — работает ровно до момента, пока маршрутов не станет два десятка. Дальше начинается: дублирование валидации, невозможность закешировать маршруты, замыкания, которые нельзя переиспользовать. Разберём альтернативы и цену каждой.
Что происходит между URL и ответом
Когда приходит запрос GET /articles/42, Laravel сопоставляет путь с зарегистрированными маршрутами по порядку объявления. Первый подошедший маршрут выигрывает — это важно, потому что статичный /articles/create, объявленный после /articles/{article}, никогда не сработает: create уедет в параметр {article}.
После сопоставления Laravel прогоняет запрос через middleware группы, резолвит параметры маршрута и вызывает метод контроллера. Возврат из метода — строку, массив, view() или JsonResource — фреймворк превращает в HTTP-ответ.
Маршрут по замыканию против контроллера
Замыкание удобно для одного-двух эндпоинтов и ломает кеш маршрутов: команда php artisan route:cache упадёт с ошибкой, если хоть один маршрут объявлен через замыкание. На проде кеш маршрутов экономит десятки миллисекунд на каждом запросе, поэтому контроллер тут не вкусовщина.
// routes/web.php — Laravel 11.x
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
// Плохо для прода: route:cache на этом сломается
Route::get('/articles/{article}', function (int $id) {
return App\Models\Article::findOrFail($id);
});
// Так кешируется и переиспользуется
Route::get('/articles/{article}', [ArticleController::class, 'show'])
->name('articles.show');
Route model binding: где экономятся запросы и где теряются
Если в сигнатуре метода объявить параметр типом модели, Laravel сам достанет запись из базы по значению из URL. Это route model binding, и он убирает ручной findOrFail из каждого метода.
namespace App\Http\Controllers;
use App\Models\Article;
class ArticleController extends Controller
{
// $article приедет уже загруженным из базы по {article}
public function show(Article $article)
{
// Если записи нет — Laravel сам вернёт 404 до входа в метод
return view('articles.show', ['article' => $article]);
}
}
Имя параметра в маршруте ({article}) должно совпадать с именем аргумента метода ($article) — иначе binding не сработает и вы получите null без явной ошибки. Это первая по частоте причина «а почему у меня модель пустая».
Route model binding делает один запрос к базе на резолв параметра. Если внутри метода вы обращаетесь к связям без eager loading, получаете классический N+1 — и binding тут ни при чём, но след ведёт именно к нему.
Поиск по полю, а не по ID
По умолчанию binding ищет по первичному ключу. Чтобы искать по slug, укажите поле прямо в маршруте:
Route::get('/articles/{article:slug}', [ArticleController::class, 'show'])
->name('articles.show');
Синтаксис {article:slug} доступен начиная с Laravel 7. Поле slug при этом стоит проиндексировать в БД — иначе на большой таблице каждый резолв станет full scan.
Route::resource: удобство и его цена
Одна строка Route::resource объявляет сразу семь маршрутов CRUD. Экономит буквы, но прячет реальный набор эндпоинтов от того, кто читает код через полгода.
// Разворачивается в 7 маршрутов
Route::resource('articles', ArticleController::class);
// Только нужное — явнее и безопаснее
Route::resource('articles', ArticleController::class)
->only(['index', 'show', 'store', 'update', 'destroy']);
Какие маршруты и методы создаёт resource:
| HTTP-метод | URI | Метод контроллера | Имя маршрута |
|---|---|---|---|
| GET | /articles | index | articles.index |
| GET | /articles/create | create | articles.create |
| POST | /articles | store | articles.store |
| GET | /articles/{article} | show | articles.show |
| GET | /articles/{article}/edit | edit | articles.edit |
| PUT/PATCH | /articles/{article} | update | articles.update |
| DELETE | /articles/{article} | destroy | articles.destroy |
Для JSON API методы create и edit (они отдают формы) не нужны — используйте Route::apiResource, он объявляет пять маршрутов без форм.
Когда что выбирать
- Явные
Route::get/post— когда набор действий нестандартный, URI не укладывается в CRUD или маршрутов немного. Читается без знания магии resource. Route::resource— классический CRUD с формами внутри одного контроллера. Обязательно с->only()или->except(), чтобы не плодить эндпоинты, которых нет в контроллере.Route::apiResource— то же для API без форм.
Валидацию — из контроллера в Form Request
Складывать $request->validate([...]) в каждый метод — путь к дублированию. Отдельный класс запроса выносит правила и авторизацию из контроллера:
// php artisan make:request StoreArticleRequest
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreArticleRequest extends FormRequest
{
public function authorize(): bool
{
// true — иначе запрос отклонится с 403 до валидации
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
}
public function store(StoreArticleRequest $request)
{
// Сюда попадём, только если валидация прошла
$article = Article::create($request->validated());
return redirect()->route('articles.show', $article);
}
Частая ловушка: забыли вернуть true из authorize() — и все запросы летят в 403. По умолчанию сгенерированный класс возвращает false, так что после make:request это надо поправить сразу.
Что ломается чаще всего
- Порядок маршрутов. Статичный сегмент, объявленный после маршрута с параметром той же глубины, недостижим. Общее правило: конкретное — выше, параметризованное — ниже.
- Несовпадение имени параметра и аргумента.
{post}в маршруте иArticle $articleв методе — binding молча дастnull. - Замыкание в маршрутах +
route:cache. Кеш не соберётся. Ошибка при деплое, не в разработке — поэтому её ловят поздно. - Ручной
findOrFailпри уже подключённом binding. Два запроса вместо одного на ровном месте. - Тип запроса. HTML-формы не умеют слать PUT/PATCH/DELETE напрямую — нужен скрытый метод через
@method('PUT')в шаблоне Blade, иначе запрос уйдёт как POST и не совпадёт с маршрутом.
Что делать дальше
Прогоните php artisan route:list — она покажет фактический набор маршрутов, их middleware и привязанные методы; это первое место, куда смотреть при «маршрут не находится». Перед деплоем проверьте, что php artisan route:cache собирается без ошибок — это выявит оставшиеся замыкания. Дальше стоит разобраться с политиками авторизации (Policy) и с JsonResource для формирования ответов API — это следующий слой поверх связки маршрут-контроллер. Инвалидация и прогрев кеша маршрутов при zero-downtime деплое — отдельная тема, здесь её не разворачиваем.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Laravel Routing — официальная документация, Laravel Controllers — официальная документация
Частые вопросы
У меня Laravel 10, а не 11 — примеры подойдут?
Почему модель в контроллере приходит пустой (null)?
Route::resource или явные маршруты — что правильнее?
Что делать, если route:cache падает при деплое?
Как искать модель по slug, а не по id?
Почему PUT или DELETE из формы не срабатывает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.