Маршруты для работы с продуктами (product) в Laravel: от простого к сложному
Маршруты для работы с продуктами в Laravel
Маршруты (routes) в Laravel определяют, как приложение отвечает на HTTP-запросы. Для типового CRUD над сущностью Product (продукт) фреймворк предлагает несколько подходов. Ниже разобраны основные варианты с примерами и пояснениями.
Стандартный ресурсный маршрут (Route::resource)
Это самый лаконичный способ создать семь RESTful маршрутов для модели Product: index, create, store, show, edit, update, destroy. Laravel автоматически свяжет URI с методами контроллера.
// routes/web.php
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
После этого можно проверить список сгенерированных маршрутов командой php artisan route:list. Все URI будут начинаться с /products, а имена маршрутов - products.*. Например, products.index, products.show.
Возможная проблема:
Если маршрутов слишком много или некоторые не нужны (например, create/edit для API), использование resource создаёт лишние эндпоинты. Также при неправильном порядке маршрутов может возникнуть конфликт с другими маршрутами.
Решение:
Использовать only или except для фильтрации. Например:
Route::resource('products', ProductController::class)->only(['index', 'show', 'store']);
Как определить только нужные маршруты для продуктов?
Если стандартный resource не подходит, можно определить маршруты вручную. Это даёт полный контроль над URI и методами.
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');
Типичная ошибка - конфликт имён при использовании одинаковых URI.
Важно, чтобы маршрут для show (с параметром {product}) не перекрывал статичный маршрут /products/create. Поэтому Laravel рекомендует сначала определять статичные маршруты, а затем динамические с параметрами. В приведённом выше порядке это соблюдено.
Как создать API-маршруты для продуктов без форм?
Для API обычно не нужны create и edit (GET для форм). Используется Route::apiResource:
Route::apiResource('products', ProductController::class);
Он генерирует только index, store, show, update, destroy. Результат можно проверить через route:list.
Проблема: при использовании apiResource в routes/web.php маршруты всё равно доступны, но могут мешать. Лучше размещать в routes/api.php.
Как добавить вложенные маршруты для отзывов к продукту?
Для связи Product -> Review (один ко многим) применяется вложенный ресурс:
Route::resource('products.reviews', ReviewController::class);
Это создаст маршруты вида /products/{product}/reviews, /products/{product}/reviews/{review} и т.д. Контроллер ReviewController получает два параметра: {product} и {review}.
Возможная ошибка: если не настроить явное связывание модели (Route::model), Laravel будет искать Review по id глобально, что может привести к путанице. Вложенные ресурсы лучше использовать с shallow:
Route::resource('products.reviews', ReviewController::class)->shallow();
Тогда уникальные маршруты (show, edit, update, destroy) станут /reviews/{review}, без лишнего product.
Как сгруппировать маршруты продуктов под общим middleware?
Группы маршрутов помогают объединить общие атрибуты - middleware, префикс, пространство имён.
Route::prefix('admin')->name('admin.')->middleware('auth')->group(function () {
Route::resource('products', ProductController::class);
});
Теперь все маршруты продуктов будут начинаться с /admin/products, их имена - admin.products.*, и доступ только для аутентифицированных пользователей.
Проблема: если внутри группы определить маршрут с таким же именем, как снаружи, возникнет конфликт. Рекомендуется использовать уникальные префиксы.
Расширенные примеры работы с маршрутами продуктов
Явное связывание модели с кастомным ключом
По умолчанию Laravel связывает параметр маршрута {product} с моделью Product по полю id. Чтобы изменить это на поле slug, применяется метод routeKeyName или явная регистрация в App\Providers\RouteServiceProvider.
// App\Providers\RouteServiceProvider
public function boot(): void
{
parent::boot();
Route::model('product', App\Models\Product::class);
// Или с кастомным разрешением:
Route::bind('product', function ($value) {
return App\Models\Product::where('slug', $value)->firstOrFail();
});
}
Теперь в маршруте /products/{product} параметр будет преобразован в модель по полю slug.
// URL: /products/laptop-asus-2024
// Эквивалент: Product::where('slug', 'laptop-asus-2024')->firstOrFail()
Проблема: если такой записи не существует, будет выброшено исключение ModelNotFoundException. Обработка может быть выполнена через глобальный обработчик исключений или кастомный запрос.
Необязательные параметры в маршруте продукта
Иногда требуется, чтобы параметр {product} мог отсутствовать. Например, для фильтрации по категории.
Route::get('/products/{category?}', [ProductController::class, 'byCategory'])
->where('category', '[a-z]+')
->name('products.byCategory');
В контроллере:
public function byCategory(?string $category = null)
{
if ($category) {
$products = Product::whereHas('category', fn($q) => $q->where('slug', $category))->get();
} else {
$products = Product::all();
}
return view('products.index', compact('products'));
}
// /products - выводит все продукты // /products/electronics - выводит продукты категории 'electronics'
Типичная ошибка: если не указать where с регулярным выражением, параметр может содержать слэши и сломать маршрут. Всегда лучше явно ограничивать допустимые символы.
Использование Route::redirect для редиректа со старого URL на новый
Чтобы перенаправить /old-products на /products с кодом 301:
Route::redirect('/old-products', '/products', 301);
Это особенно полезно при рефакторинге маршрутов, когда нужно сохранить обратную совместимость.
// Запрос GET /old-products -> ответ 301 и Location: /products
Группа маршрутов с ограничением по поддомену
Предположим, продукты должны быть доступны только через поддомен shop.example.com. Группа с фильтрацией поддомена:
Route::domain('shop.example.com')->group(function () {
Route::resource('products', ProductController::class);
});
Теперь все маршруты products.* будут работать только на данном поддомене. В локальной среде можно использовать php artisan serve и /etc/hosts.
Проблема: если поддомен не настроен в DNS, маршруты не сработают. Также необходимо, чтобы Laravel принимал запросы с этого поддомена (настройка в AppServiceProvider или RouteServiceProvider).
Маршруты продуктов с мягким удалением (SoftDeletes)
Для работы с удалёнными продуктами (trashed) можно добавить отдельный маршрут с параметром, который явно ищет в том числе мягко удалённые записи.
Route::get('/products/trashed', [ProductController::class, 'trashed'])->name('products.trashed');
Route::patch('/products/{product}/restore', [ProductController::class, 'restore'])->name('products.restore');
В контроллере:
public function trashed()
{
$products = Product::onlyTrashed()->get();
return view('products.trashed', compact('products'));
}
public function restore(Product $product)
{
$product->restore();
return redirect()->route('products.index');
}
// GET /products/trashed - список удалённых // PATCH /products/1/restore - восстановление
Важно: маршрут restore должен быть определён после resource или использовать другой URI, чтобы не конфликтовать с show. Рекомендуется размещать такие кастомные маршруты до resource, чтобы они имели приоритет.
Именованные маршруты с генерацией URL
После определения маршрута, например Route::resource('products', ...), можно генерировать URL в шаблонах или контроллерах:
$url = route('products.show', ['product' => 42]);
// Результат: /products/42
$url = route('products.edit', ['product' => 10]);
// /products/10/edit
Если используется кастомное связывание по slug, передавать нужно slug:
$url = route('products.show', ['product' => 'laptop-asus']);
// /products/laptop-asus
Проблема: если у модели несколько ключевых полей и используется привязка не по id, нужно убедиться, что в маршруте параметр {product} правильно разрешается. Иначе Laravel может выбросить исключение или сгенерировать неверный URL.