Создание маршрутов продуктов: лучшие практики PHP

Раздел: Веб-разработка на PHP -> Маршрутизация PHP

Основные подходы к маршрутизации продуктов

Наиболее эффективное решение - использование библиотеки FastRoute с групповыми маршрутами и внедрением контроллеров.

Этот вариант обеспечивает высокую производительность, читаемость и поддержку RESTful практик. Маршруты группируются по префиксу /products, каждый HTTP-метод обрабатывается отдельным обработчиком.


// composer require nikic/fast-route
// config/routes.php
use FastRoute\RouteCollector;

return function (RouteCollector $r) {
    $r->addGroup('/products', function (RouteCollector $r) {
        $r->addRoute('GET', '', 'ProductController::index');      // список
        $r->addRoute('GET', '/{id:\d+}', 'ProductController::show'); // один продукт
        $r->addRoute('POST', '', 'ProductController::store');     // создание
        $r->addRoute('PUT', '/{id:\d+}', 'ProductController::update');
        $r->addRoute('DELETE', '/{id:\d+}', 'ProductController::destroy');
    });
};
  

Обработчик получает параметры маршрута и может вызывать соответствующий метод контроллера. Библиотека сама разбирает путь и выполняет подстановку параметров.

Частая ошибка:

Неправильное использование регулярных выражений в шаблоне {id:\d+} приводит к тому, что маршрут не срабатывает для числовых идентификаторов. Решение - всегда экранировать обратную косую черту в PHP-строке: '{id:\\d+}'.

Как реализовать маршруты продуктов без внешних библиотек, используя только встроенные функции?

Можно написать простой маршрутизатор на основе parse_url и регулярных выражений. Это подходит для маленьких проектов, где не требуется высокая производительность.


$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];

// База маршрутов
$routes = [
    'GET /products' => 'indexAction',
    'GET /products/(\\d+)' => 'showAction',
    'POST /products' => 'storeAction',
];

$matched = false;
foreach ($routes as $pattern => $action) {
    list($routeMethod, $routePath) = explode(' ', $pattern, 2);
    if ($method !== $routeMethod) continue;
    $regex = '#^' . $routePath . '$#';
    if (preg_match($regex, $uri, $matches)) {
        array_shift($matches); // удаляем полное совпадение
        call_user_func_array($action, $matches);
        $matched = true;
        break;
    }
}
if (!$matched) {
    header('HTTP/1.1 404 Not Found');
    echo 'Страница не найдена';
}
  

Проблема такого подхода:

Нет поддержки сложной вложенности и middleware. Порядок маршрутов важен - более конкретные должны быть объявлены раньше общих. Ошибка - забыть удалить полное совпадение из $matches, что передаёт лишний параметр в функцию.

Как добавить вложенные маршруты для продуктов, связанных с категориями?

Использование групповых маршрутов в FastRoute с вложенными префиксами:


$r->addGroup('/categories/{catId:\\d+}', function (RouteCollector $r) {
    $r->addGroup('/products', function (RouteCollector $r) {
        $r->addRoute('GET', '', 'CategoryProductController::index');
        $r->addRoute('GET', '/{prodId:\\d+}', 'CategoryProductController::show');
    });
});
  

Параметры catId и prodId передаются обработчику. В другом варианте (чистый PHP) придётся вручную извлекать сегменты URI.

Типичная ошибка:

Неверная вложенность group - забывают передать RouteCollector в анонимную функцию. Решение - всегда проверять сигнатуру колбэка.

Как маршрутизировать продукты с использованием query-параметров (например, фильтрация)?

Метод GET /products?category=1&page=2 обрабатывается тем же маршрутом, параметры извлекаются из $_GET.


$r->addRoute('GET', '/products', 'ProductController::index');
// В контроллере:
function index() {
    $category = $_GET['category'] ?? null;
    $page = $_GET['page'] ?? 1;
    // ... логика
}
  

Проблема:

Смешивание логики маршрутизации и бизнес-логики. Лучше передавать параметры через сам маршрут (например, /products/category/1/page/2) или использовать middleware для валидации.

Как обеспечить RESTful маршруты для продуктов с авторизацией?

Добавление middleware (проверку токена или сессии) в группу маршрутов:


// Используя FastRoute и собственный диспетчер
$dispatcher = FastRoute\simpleDispatcher(function(RouteCollector $r) {
    $r->addGroup('/api/products', ['middleware' => 'auth'], function (RouteCollector $r) {
        $r->addRoute('GET', '', ['ProductController', 'index']);
        $r->addRoute('POST', '', ['ProductController', 'store']);
    });
});
  

В реальных фреймворках (Laravel, Symfony) это делается через группы middleware.

Ошибка:

Пытаться реализовать middleware внутри анонимной функции без понимания порядка вызовов. В FastRoute нет встроенного middleware, поэтому требуется обёртка.

Какие проблемы возникают при использовании симфони-роутинга в чистом PHP?

Symfony Routing требует PSR-7 сообщений и дополнительных абстракций, что усложняет простые проекты.


use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();
$routes->add('product_index', new Route('/products', [
    '_controller' => 'ProductController::index',
    'methods' => ['GET']
]));
  

Проблема - необходимость настройки UrlMatcher и контекста запроса. Ошибки возникают при несовпадении формата параметров (например, {id} без ограничения типа).

Типичная ошибка:

Забывают указать метод HTTP в опциях маршрута, что приводит к тому, что POST-запрос обрабатывается как GET. Решение - всегда явно прописывать 'methods'.

Расширенные примеры маршрутов продуктов

Пример 1. Вложенные ресурсы с пагинацией и фильтрацией через сегменты URL

Маршрут: /api/v2/categories/{catId}/products?status=active&limit=10 и /api/v2/categories/{catId}/products/{prodId}

Пример

// FastRoute
$r->addGroup('/api/v2/categories/{catId:\d+}', function (RouteCollector $r) {
    $r->addGroup('/products', function (RouteCollector $r) {
        $r->addRoute('GET', '', 'CategoryProductController::index');
        $r->addRoute('GET', '/{prodId:\d+}', 'CategoryProductController::show');
        $r->addRoute('POST', '', 'CategoryProductController::store');
    });
});

В контроллере:

Пример

class CategoryProductController {
    public function index($catId) {
        $status = $_GET['status'] ?? 'all';
        $limit  = (int)($_GET['limit'] ?? 20);
        // выборка продуктов из категории $catId с фильтрацией
        echo "Категория $catId, статус $status, лимит $limit";
    }
    public function show($catId, $prodId) {
        echo "Продукт $prodId из категории $catId";
    }
}

Результат вызова GET /api/v2/categories/5/products?status=active:

Категория 5, статус active, лимит 20

Пример 2. Использование именованных маршрутов для генерации URL продуктов

FastRoute не поддерживает именованные маршруты напрямую, но можно реализовать через хранение в массиве. В Symfony Routing это встроено.

Пример

// Симфони роутинг
$route = new Route('/products/{id}', [
    '_controller' => 'ProductController::show',
], ['id' => '\d+']);
$route->setMethods(['GET']);
$routes->add('product_show', $route);

// Генерация URL
use Symfony\Component\Routing\Generator\UrlGenerator;
$generator = new UrlGenerator($routes, new RequestContext());
echo $generator->generate('product_show', ['id' => 42]);
/products/42

Пример 3. Маршруты с middleware для проверки прав доступа к продуктам

В самописном решении middleware оборачивает обработчик:

Пример

function authMiddleware(callable $handler) {
    return function (...$params) use ($handler) {
        if (!isset($_SESSION['user'])) {
            header('HTTP/1.1 401 Unauthorized');
            exit('Требуется авторизация');
        }
        return $handler(...$params);
    };
}

$routes = [
    'GET /products' => authMiddleware('ProductController::index'),
    'POST /products' => authMiddleware('ProductController::store'),
];

Проблема: сложно компоновать несколько middleware. Решение - использовать цепочку вызовов или библиотеку Middleware.

Пример 4. Обработка ошибок 404 для несуществующих продуктов с кастомной страницей

Пример

$r->addRoute('GET', '/products/{id:\d+}', function ($id) {
    $product = findProductById($id);
    if (!$product) {
        // выбросить исключение или вернуть 404
        http_response_code(404);
        echo json_encode(['error' => 'Продукт не найден']);
        return;
    }
    echo json_encode($product);
});

Более продвинутый способ - использовать middleware для отлова исключений:

Пример

class NotFoundException extends \Exception {}

$container = function($uri, $method) {
    try {
        // диспетчеризация маршрута
    } catch (NotFoundException $e) {
        header('HTTP/1.1 404 Not Found');
        echo '

404 - Продукт не найден

'; } };

Пример 5. Маршруты для массовых операций над продуктами (batch)

Маршрут POST /products/batch для массового создания/обновления:

Пример

$r->addRoute('POST', '/products/batch', 'ProductController::batchStore');

// в контроллере
function batchStore() {
    $data = json_decode(file_get_contents('php://input'), true);
    foreach ($data as $product) {
        // сохранение
    }
    echo 'Обработано ' . count($data) . ' продуктов';
}

Вызов curl -X POST -H "Content-Type: application/json" -d '[{"name":"A"},{"name":"B"}]' http://example.com/products/batch

Обработано 2 продуктов

Пример 6. Динамический роутинг продуктов на основе slug вместо id

Пример

$r->addRoute('GET', '/products/{slug:[a-z0-9-]+}', 'ProductController::showBySlug');

function showBySlug($slug) {
    $product = getProductBySlug($slug);
    if (!$product) {
        http_response_code(404);
        echo 'Продукт с таким URL не найден';
        return;
    }
    echo "Продукт: {$product['name']}";
}

Ошибка: забыть экранировать дефис в регулярном выражении. Правильно: [a-z0-9-]+ - дефис должен быть в конце или начале, иначе интерпретируется как диапазон.

Пример 7. Разделение маршрутов для админки и публичного API продуктов

Пример

$r->addGroup('/admin', function (RouteCollector $r) {
    $r->addGroup('/products', function (RouteCollector $r) {
        $r->addRoute('GET', '', 'Admin\ProductController::index');
        $r->addRoute('POST', '', 'Admin\ProductController::store');
    });
});

$r->addGroup('/api', function (RouteCollector $r) {
    $r->addGroup('/products', function (RouteCollector $r) {
        $r->addRoute('GET', '', 'Api\ProductController::index');
    });
});

Разные пространства имён контроллеров упрощают поддержку.

- Index php route product category (маршрут категории продуктов)
- Products php route (маршруты продуктов)
- Index php route checkout checkout (маршрут оформления заказа)
- Index php route (маршрут index.php)

Маршруты продуктов - comments

En
Products php route (php)