Маршрутизация по категориям: обработка URL-путей в PHP

Раздел: Веб-разработка на PHP -> Маршрутизация в 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) выведет: Текущая категория: Посуда
Результат при корректной БД:
Текущая категория: Посуда

Путь категории в PHP - comments

En
Path php category (php)