Маршрутизация по категориям: обработка URL-путей в PHP
Основные подходы к обработке категорий в URL
Наиболее эффективное решение
Создание собственного легковесного маршрутизатора с использованием регулярных выражений позволяет обрабатывать категории любой вложенности. Маршрутизатор получает URI, разбивает его на сегменты и сопоставляет с заранее заданными шаблонами. Для категорий удобно захватывать переменное количество сегментов с помощью .* или задавать фиксированные уровни.
Пример минимального маршрутизатора:
<?php
// Определение маршрутов
$routes = [
'/' => 'home',
'/category/{path}' => 'category_view',
'/category/{path}/product/{id}' => 'product_view',
];
// Разбор текущего URI
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = rtrim($uri, '/');
if ($uri === '') $uri = '/';
// Функция сопоставления
function matchRoute($uri, $routes) {
foreach ($routes as $pattern => $handler) {
// Преобразуем {param} в именованные группы в регулярном выражении
$regex = preg_replace('/\{([a-z_]+)\}/', '(?P<$1>[^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
if (preg_match($regex, $uri, $matches)) {
return [
'handler' => $handler,
'params' => array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY)
];
}
}
return null;
}
$result = matchRoute($uri, $routes);
if ($result) {
echo 'Handler: ' . $result['handler'] . "\n";
print_r($result['params']);
} else {
http_response_code(404);
echo 'Страница не найдена';
}
Php dispatch (php dispatch (маршрутизация))
Объяснение шагов:
- Массив
$routesсодержит шаблоны URL с параметрами в фигурных скобках. - Из каждого шаблона строится регулярное выражение, где
{path}заменяется на группу, захватывающую любой символ, кроме слэша. - Функция
preg_matchсопоставляет URI с выражением, извлекая значения параметров. - Для категорий с переменной глубиной можно использовать
{path}– он захватит только один сегмент. Если нужно захватить несколько уровней, применяют{path:.*}.
Типичные ошибки и их исправление
- Проблема: URL содержит закодированные символы (например, пробелы как %20). Решение: использовать
urldecode()для извлечённых параметров. - Проблема: Регулярное выражение слишком жадное – захватывает больше, чем нужно. Решение: использовать нежадные квантификаторы
*?или явные границы. - Проблема: Путь категории может содержать слэши, а маршрутизатор ломается на их обработке. Решение: для вложенных категорий использовать паттерн
{category:.*}, который захватывает всё до конца строки.
Цели и случаи использования
Данный подход подходит для проектов любой сложности, где требуется гибкость и скорость. Он легко расширяется добавлением новых маршрутов, поддерживает вложенные категории и работает без внешних зависимостей.
Альтернативные подходы
Как сделать простую маршрутизацию на основе $_GET?
Классический вариант – передавать категорию через параметр запроса, например ?cat=electronics/laptops. Это самый простой способ, но он не обеспечивает красивых URL.
// Получение всех сегментов категории
$categoryPath = $_GET['cat'] ?? '';
$segments = explode('/', $categoryPath);
echo 'Root category: ' . ($segments[0] ?? 'none');
Path php category (путь категории в php)
Проблемы
- Не поддерживает чистые URL (без index.php?).
- Сложно обрабатывать вложенные категории с переменной глубиной без дополнительной логики.
Как использовать explode для разбора пути категории напрямую?
Можно взять URI, убрать ведущий слэш и разбить на массив. Первый элемент – категория первого уровня, второй – второго и т.д. Этот вариант подходит для сайтов с фиксированной глубиной категорий.
$uri = $_SERVER['REQUEST_URI'];
$uri = trim(parse_url($uri, PHP_URL_PATH), '/');
$segments = explode('/', $uri);
$categoryLevel1 = $segments[0] ?? null;
$categoryLevel2 = $segments[1] ?? null;
$productSlug = $segments[2] ?? null;
if ($categoryLevel1 === 'products' && $categoryLevel2) {
echo "Вы находитесь в категории: $categoryLevel2";
}
Php route product product path (маршрут php для товара)
Проблема
- Жёсткая привязка к количеству сегментов – сложно менять структуру URL.
- Нет возможности обрабатывать необязательные части пути.
Как применить маршрутизацию на основе регулярных выражений с именованными группами?
Этот вариант похож на решение из «наиболее эффективного», но без динамического построения. Маршруты задаются готовыми регулярными выражениями, что даёт полный контроль, но требует больше писанины.
$routes = [
'#^/category/(?P<cat>[a-z0-9-/]+)$#' => 'categoryView',
'#^/category/(?P<cat>[a-z0-9-/]+)/page/(?P<page>\d+)$#' => 'categoryPage',
];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($routes as $regex => $handler) {
if (preg_match($regex, $uri, $matches)) {
call_user_func($handler, $matches);
break;
}
}
function categoryView($params) {
echo 'Категория: ' . $params['cat'];
}
function categoryPage($params) {
echo 'Категория: ' . $params['cat'] . ', страница: ' . $params['page'];
}
Цель: точный контроль над форматом URL, поддержка необязательных сегментов (например, страница пагинации). Случаи использования – сайты с чётко заданными шаблонами категорий.
Как организовать иерархическую маршрутизацию с помощью дерева категорий?
Для больших каталогов с тысячами категорий эффективно использовать древовидную структуру в памяти или в базе данных. Маршрутизатор получает все сегменты URI, последовательно ищет каждую категорию в дереве и выполняет действия в зависимости от найденного узла.
$categoryTree = [
'electronics' => [
'title' => 'Электроника',
'children' => [
'laptops' => ['title' => 'Ноутбуки', 'children' => []],
'phones' => ['title' => 'Телефоны', 'children' => []],
]
],
];
$segments = explode('/', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'));
$current = $categoryTree;
$found = true;
foreach ($segments as $slug) {
if (isset($current['children'][$slug])) {
$current = $current['children'][$slug];
} else {
$found = false;
break;
}
}
if ($found) {
echo 'Текущая категория: ' . $current['title'];
} else {
http_response_code(404);
}
Ошибки и их решение
- Если дерево загружается из базы данных, возможны повторные запросы при каждом запросе. Решение: кэшировать дерево в файле или Redis.
- Не учтён регистр символов – slug может быть case-insensitive. Решение: приводить к нижнему регистру.
Цель: естественная поддержка вложенных категорий и быстрая проверка существования пути. Используется в интернет-магазинах и корпоративных порталах с глубокой структурой.
Ниже приведены расширенные примеры с полными сценариями и выводами.
<?php
// Пример 1: Маршрутизация с поддержкой языка и категории
$routes = [
'/{lang}/{category}/' => 'showCategory',
'/{lang}/{category}/{subcat}' => 'showSubcategory',
];
function route($uri, $routes) {
foreach ($routes as $pattern => $handler) {
$regex = preg_replace('/\{([a-z_]+)\}/', '(?P<$1>[a-z]{2,})', $pattern);
$regex = '#^' . $regex . '$#';
if (preg_match($regex, $uri, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
return $handler($params);
}
}
return null;
}
$uri = '/en/electronics/laptops';
route($uri, $routes);
function showCategory($params) {
echo 'Язык: ' . $params['lang'] . '\n';
echo 'Категория: ' . $params['category'];
}
function showSubcategory($params) {
echo 'Язык: ' . $params['lang'] . '\n';
echo 'Категория: ' . $params['category'] . '\n';
echo 'Подкатегория: ' . $params['subcat'];
}
// Вывод для /en/electronics/laptops:
// Язык: en
// Категория: electronics
// Подкатегория: laptops
Результат работы: Язык: en Категория: electronics Подкатегория: laptops
<?php
// Пример 2: Категория с пагинацией и необязательным суффиксом
$uri = '/category/компьютеры/аксессуары/страница/3';
$patterns = [
'#^/category/(?P<cat1>[^/]+)/(?P<cat2>[^/]+)/страница/(?P<page>\d+)$#' => 'catPage',
'#^/category/(?P<cat1>[^/]+)/(?P<cat2>[^/]+)$#' => 'catSimple',
];
foreach ($patterns as $regex => $handler) {
if (preg_match($regex, $uri, $m)) {
call_user_func($handler, $m);
break;
}
}
function catPage($m) {
echo 'Категория 1: ' . $m['cat1'] . "\n";
echo 'Категория 2: ' . $m['cat2'] . "\n";
echo 'Страница: ' . $m['page'];
}
function catSimple($m) {
echo 'Категория 1: ' . $m['cat1'] . "\n";
echo 'Категория 2: ' . $m['cat2'] . "\n";
echo 'Страница не указана, показываем первую';
}
// Вывод для /category/компьютеры/аксессуары/страница/3
Результат: Категория 1: компьютеры Категория 2: аксессуары Страница: 3
<?php
// Пример 3: Использование FastRoute для категорий (внешняя библиотека)
require 'vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/category[/{path:.*}]', 'categoryHandler');
});
$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];
echo 'Категория: ' . ($vars['path'] ?? 'корень');
break;
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo 'Не найдено';
break;
}
// При запросе /category/электроника/ноутбуки выведет: Категория: электроника/ноутбуки
Результат для /category/электроника/ноутбуки: Категория: электроника/ноутбуки
<?php
// Пример 4: Категория с проверкой существования в базе (SQLite)
$db = new PDO('sqlite:categories.db');
$uri = '/дом/кухня/посуда';
$segments = explode('/', trim($uri, '/'));
// Поиск в цикле по сегментам
$parentId = null;
$currentName = '';
foreach ($segments as $slug) {
$stmt = $db->prepare('SELECT id, name FROM categories WHERE slug = ? AND parent_id ' . ($parentId ? '= ?' : 'IS NULL'));
if ($parentId) {
$stmt->execute([$slug, $parentId]);
} else {
$stmt->execute([$slug]);
}
$cat = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$cat) {
http_response_code(404);
echo 'Категория не найдена';
exit;
}
$parentId = $cat['id'];
$currentName = $cat['name'];
}
echo 'Текущая категория: ' . $currentName;
// При наличии в БД категорий 'дом' (id=1), 'кухня' (parent=1), 'посуда' (parent=2) выведет: Текущая категория: Посуда
Результат при корректной БД: Текущая категория: Посуда