Маршруты для работы с продуктами (product) в Laravel: от простого к сложному

Раздел: Фреймворки PHP -> 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.

Маршруты в Laravel (product) - comments

En
Php route product product (php)