Маршрутизация категории товара: от простого к продвинутому
Основные подходы к маршрутизации категорий товаров
Маршрут для отображения товаров определённой категории является типовой задачей веб-приложений. URL обычно имеет вид /category/{slug} или /products/category/{id}. Ниже рассматриваются несколько способов реализации такого маршрута на чистом PHP, без использования фреймворков, и с использованием современных библиотек.
Основное эффективное решение – FastRoute
Как реализовать маршрут категории товара с поддержкой параметров, производительностью и простотой?
Библиотека FastRoute (nikic/FastRoute) предоставляет быстрый и гибкий роутер. Она подходит для проектов, где не требуется полный фреймворк, но нужна надёжная маршрутизация.
// Установка: composer require nikic/fast-route
// index.php
require 'vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/category/{slug}', 'Controller@category');
});
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// Вызов контроллера с параметрами
list($class, $method) = explode('@', $handler);
$controller = new $class;
echo $controller->$method($vars['slug']);
break;
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo 'Страница не найдена';
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo 'Метод не разрешён';
break;
}
Типичные ошибки:
- Неверный порядок маршрутов: FastRoute обрабатывает маршруты в порядке добавления, поэтому более специфичные маршруты должны быть перед общими.
- Забыт
parse_url()– без него параметры запроса (query string) могут повлиять на сопоставление. - Отсутствие кэширования: для production рекомендуется кэшировать скомпилированные маршруты через
FastRoute\cachedDispatcher().
Цель: быстрая разработка с минимальным кодом, поддержка переменных сегментов, кэширование для высокой производительности.
Вариант 1: Ручной разбор URL с explode и switch
Как обработать маршрут категории без внешних зависимостей, используя лишь встроенные функции PHP?
$uri = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
$segments = explode('/', $uri);
if (count($segments) >= 2 && $segments[0] === 'category') {
$slug = $segments[1];
// Загрузка товаров категории
echo "Товары категории: $slug";
} else {
http_response_code(404);
echo 'Страница не найдена';
}
Проблемы и ограничения:
- Невозможность гибко задавать шаблоны (например,
/category/{slug}/page/{num}без дополнительных условий). - При добавлении новых маршрутов код быстро разрастается и становится трудно поддерживаемым.
- Нет поддержки методов HTTP (GET, POST и т.д.).
Когда использовать: только для самых простых приложений с одним-двумя маршрутами, где не планируется расширение.
Вариант 2: Использование регулярных выражений (preg_match)
Как реализовать маршрут категории с более сложными условиями, например, с обязательным префиксом и опциональными параметрами?
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$pattern = '#^/category/(?P<slug>[a-zA-Z0-9\-]+)(/(?P<page>\d+))?$#';
if (preg_match($pattern, $uri, $matches)) {
$slug = $matches['slug'];
$page = $matches['page'] ?? 1;
echo "Категория $slug, страница $page";
} else {
http_response_code(404);
echo 'Не найдено';
}
Сложности:
- Регулярные выражения трудно читать и отлаживать.
- При большом количестве маршрутов производительность падает из-за перебора всех паттернов.
- Нет автоматического кэширования – каждый запрос выполняет preg_match.
Когда использовать: если нужно быстрое прототипирование и маршруты не меняются, но требуется гибкость переменных сегментов.
Вариант 3: Роутер из фреймворка (Laravel / Symfony)
Как профессионально организовать маршрут категории в рамках полноценного фреймворка?
Пример для Laravel (файл routes/web.php):
Route::get('/category/{slug}', 'ProductController@category');
Symfony (YAML):
# config/routes.yaml
product_category:
path: /category/{slug}
controller: App\Controller\ProductController::category
Возможные проблемы:
- Привязка к конкретному фреймворку, сложность переноса.
- Избыточность для небольших проектов.
Когда использовать: при разработке в составе Laravel, Symfony или другого фреймворка – это стандартный подход.
Вариант 4: Использование атрибутов PHP 8 (Symfony)
Как определить маршрут категории непосредственно в методе контроллера, используя современные возможности языка?
use Symfony\Component\Routing\Annotation\Route;
class ProductController
{
#[Route('/category/{slug}', name: 'product_category')]
public function category(string $slug): Response
{
// ...
}
}
Недостатки:
- Требуется конфигурация для сканирования атрибутов (например, в Symfony – Doctrine Annotation Reader).
- Нестандартный синтаксис может отпугнуть новичков.
Когда использовать: в современных проектах на Symfony 6+ или при использовании атрибутов с собственным роутером (например, с библиотекой PHP-Router).
Расширенные примеры реализации роутинга категории товара
Пример 1: Самописный роутер с поддержкой middleware и проверкой метода
// Router.php
class Router
{
private array $routes = [];
public function add(string $method, string $pattern, callable $handler, array $middlewares = []): void
{
$this->routes[] = compact('method', 'pattern', 'handler', 'middlewares');
}
public function dispatch(string $method, string $uri): void
{
foreach ($this->routes as $route) {
if ($route['method'] !== $method) continue;
$pattern = '#^' . preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $route['pattern']) . '$#';
if (preg_match($pattern, $uri, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
// Применение middleware (цепочка)
$next = function() use ($route, $params) {
call_user_func($route['handler'], $params);
};
foreach (array_reverse($route['middlewares']) as $mw) {
$next = fn() => $mw($params, $next);
}
$next();
return;
}
}
http_response_code(404);
echo "Страница не найдена";
}
}
// Использование
$router = new Router();
$authMiddleware = function($params, $next) {
if (!isset($_SESSION['user'])) {
http_response_code(401);
echo "Доступ запрещён";
return;
}
$next();
};
$router->add('GET', '/category/{slug}', function($params) {
echo "Товары категории: " . htmlspecialchars($params['slug']);
}, [$authMiddleware]);
$router->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
При GET /category/electronics? (без авторизации) – вернёт 401. При GET /category/electronics (с авторизацией) – "Товары категории: electronics"
Пример 2: Использование PSR-7 и PSR-15 для роутинга
// Требуются библиотеки: psr/http-message, psr/http-server-handler, nyholm/psr7
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Nyholm\Psr7\Response;
class ProductCategoryHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): Response
{
$slug = $request->getAttribute('slug');
$body = "Товары категории: $slug";
return new Response(200, [], $body);
}
}
// Пример с FastRoute и PSR-7 (используется middleware-диспетчер)
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/category/{slug}', ProductCategoryHandler::class);
});
// Далее – адаптация PSR-7 запроса к FastRoute, вызов handler
Этот подход позволяет легко комбинировать роутинг с любыми PSR-15 middleware.
Пример 3: Генерация URL по имени маршрута (reverse routing)
// Допустим, используется библиотека aura/router
$router = new Aura\Router\RouterContainer();
$map = $router->getMap();
$map->get('product.category', '/category/{slug}', ['controller' => 'ProductController', 'action' => 'category']);
// Генерация URL
$generator = $router->getGenerator();
$url = $generator->generate('product.category', ['slug' => 'phones']);
// Результат: /category/phones
Сгенерированный URL: /category/phones
Пример 4: Кэширование маршрутов с FastRoute в production
$cacheFile = __DIR__ . '/route_cache.php';
if (file_exists($cacheFile)) {
$dispatcher = FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/category/{slug}', 'handler');
}, ['cacheFile' => $cacheFile]);
} else {
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/category/{slug}', 'handler');
});
}
При первом запросе создаётся кэш-файл, последующие запросы загружают его, что значительно ускоряет работу.
Пример 5: Маршрут с вложенными параметрами (подкатегории)
// FastRoute: /category/{slug}/{subslug?}
$r->addRoute('GET', '/category/{slug}[/{subslug}]', 'handler');
// Обработчик получает slug и необязательный subslug
Это позволяет строить иерархические URL, например /category/electronics/phones.