Модуль маршрутов PHP: практические подходы и примеры

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

Введение в маршрутизацию на PHP

Маршрутизация (роутинг) является ключевым компонентом любого веб-приложения. Она определяет, какой код выполняется при запросе определённого URL. В данной статье рассматриваются различные подходы к реализации маршрутизации в PHP: от простых ручных решений до готовых библиотек и PSR-7 совместимых роутеров. Для каждого варианта приведены вопросы, на которые он отвечает, примеры кода, типичные ошибки и способы их исправления.

Основное эффективное решение: FastRoute

FastRoute - легковесная и высокопроизводительная библиотека, используемая в Laravel, Slim и других фреймворках. Она основана на регулярных выражениях и не зависит от дополнительных зависимостей.

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

Установка через Composer:

composer require nikic/fast-route

Php route information information information (маршрут информации (повтор))

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

<?php
require __DIR__ . '/vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addRoute('GET', '/users', 'UserListHandler');
    $r->addRoute('GET', '/users/{id:\d+}', 'UserShowHandler');
    $r->addRoute('POST', '/users', 'UserCreateHandler');
});

// Определяем обработчик
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        http_response_code(404);
        echo '404 Не найдено';
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        $allowedMethods = $routeInfo[1];
        http_response_code(405);
        echo '405 Метод не разрешён. Разрешённые: ' . implode(', ', $allowedMethods);
        break;
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        // Вызываем хендлер, передавая переменные
        if (function_exists($handler)) {
            $handler($vars);
        } else {
            // Можно использовать класс с методом
            [$class, $method] = explode('::', $handler);
            (new $class)->$method($vars);
        }
        break;
}

Index php route product category (маршрут категории продуктов)

Типичные проблемы:

  • Забыли указать метод HTTP - маршрут не сработает.
  • Ошибка в регулярном выражении параметра - FastRoute выбрасывает исключение.
  • Порядок маршрутов: более специфичные должны идти раньше общих.

Решение: всегда проверять синтаксис параметров вида {name:pattern} и тестировать dispatch.

Как сделать простой роутер на switch без внешних библиотек?

Этот подход полезен для небольших проектов или быстрых прототипов. Каждый маршрут обрабатывается внутри конструкции switch/case.

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

switch (true) {
    case $uri === '/' && $method === 'GET':
        echo 'Главная страница';
        break;
    case $uri === '/about' && $method === 'GET':
        echo 'О компании';
        break;
    case preg_match('#^/user/(\d+)$#', $uri, $matches) && $method === 'GET':
        $userId = $matches[1];
        echo "Профиль пользователя ID=$userId";
        break;
    default:
        http_response_code(404);
        echo '404';
}

Php route module (маршрут модуля)

Проблемы: быстро разрастается, трудно поддерживать, нет повторного использования кода. Ошибка: часто забывают про break.

Решение: для небольших проектов допустимо, но лучше переходить на регулярные выражения или ООП-роутер.

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

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

<?php
class Router {
    private array $routes = [];

    public function get(string $pattern, callable $handler): void {
        $this->addRoute('GET', $pattern, $handler);
    }

    public function post(string $pattern, callable $handler): void {
        $this->addRoute('POST', $pattern, $handler);
    }

    private function addRoute(string $method, string $pattern, callable $handler): void {
        $this->routes[] = compact('method', 'pattern', 'handler');
    }

    public function dispatch(string $method, string $uri): void {
        foreach ($this->routes as $route) {
            if ($route['method'] !== $method) continue;
            // Преобразуем {id} в (?P<id>[^/]+)
            $regex = preg_replace('#\{([a-zA-Z_]+)\}#', '(?P<$1>[^/]+)', $route['pattern']);
            $regex = '#^' . $regex . '$#';
            if (preg_match($regex, $uri, $matches)) {
                $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
                call_user_func($route['handler'], $params);
                return;
            }
        }
        http_response_code(404);
    }
}

$router = new Router();
$router->get('/posts/{slug}', function($params) {
    echo "Статья: " . $params['slug'];
});
$router->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

Index php route checkout checkout (маршрут оформления заказа)

Типичные ошибки: неэкранированные спецсимволы в паттерне, конфликты имён параметров, низкая производительность при большом числе маршрутов.

Решение: использовать готовые библиотеки (FastRoute, Symfony Router) для больших проектов.

Как реализовать маршрутизацию через Symfony Router для сложных приложений?

Symfony Router предоставляет мощную систему с поддержкой YAML/XML/аннотаций, кэшированием и интеграцией с компонентом HttpKernel.

composer require symfony/routing symfony/http-kernel symfony/config

Products php route (маршруты продуктов)

<?php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$routes = new RouteCollection();
$routes->add('user_show', new Route('/users/{id}', ['_controller' => 'UserController::show'], ['id' => '\d+']));

$context = new RequestContext();
$context->fromRequest(
    Symfony\Component\HttpFoundation\Request::createFromGlobals()
);

$matcher = new UrlMatcher($routes, $context);

try {
    $parameters = $matcher->match($context->getPathInfo());
    $controller = $parameters['_controller'];
    unset($parameters['_controller']);
    [$class, $method] = explode('::', $controller);
    echo (new $class)->$method($parameters);
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
    http_response_code(404);
    echo 'Не найдено';
} catch (\Exception $e) {
    http_response_code(500);
    echo 'Ошибка';
}

Проблемы: большой размер библиотеки, необходимость настройки загрузки конфигурации. Ошибка: неверный порядок загрузки роутов из разных файлов.

Решение: использовать только если проект уже использует Symfony или требует сложного кэширования.

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

1. Именованные маршруты с генерацией URL

Многие роутеры позволяют назначать имена маршрутам и генерировать по ним URL. Это упрощает рефакторинг. Пример с FastRoute (не встроено, но можно добавить):

Пример
<?php
class NamedRouteCollector extends FastRoute\RouteCollector {
    private array $namedRoutes = [];

    public function addNamedRoute($httpMethod, $route, $handler, $name): void {
        $this->addRoute($httpMethod, $route, $handler);
        $this->namedRoutes[$name] = ['method' => $httpMethod, 'route' => $route];
    }

    public function generate($name, array $params = []): string {
        if (!isset($this->namedRoutes[$name])) {
            throw new InvalidArgumentException("Route $name not found");
        }
        $route = $this->namedRoutes[$name]['route'];
        foreach ($params as $key => $value) {
            $route = str_replace('{' . $key . '}', $value, $route);
        }
        return $route;
    }
}

$collector = new NamedRouteCollector(new FastRoute\RouteParser\Std());
$collector->addNamedRoute('GET', '/user/{id}', 'handler', 'user.show');
echo $collector->generate('user.show', ['id' => 42]); // /user/42

Результат:

/user/42

2. Группировка маршрутов с префиксами

Часто нужно создать группу маршрутов с общим префиксом, например, /api/v1. FastRoute поддерживает группы через callback.

Пример
<?php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addGroup('/api/v1', function (FastRoute\RouteCollector $r) {
        $r->addRoute('GET', '/users', 'listUsers');
        $r->addRoute('POST', '/users', 'createUser');
    });
});
// GET /api/v1/users -> listUsers

3. Middleware и маршрутизация на основе PSR-15

Использование PSR-15 (Request Handler) позволяет добавлять слои middleware до и после обработчика маршрута. Пример с библиотекой league/route:

Пример
composer require league/route
Пример
<?php
use League\Route\Router;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;

$router = new Router;

$router->middleware(function ($request, $handler) {
    // Логирование запроса
    $response = $handler->handle($request);
    return $response->withHeader('X-Powered-By', 'League Route');
});

$router->map('GET', '/hello/{name}', function ($request, array $args) {
    $response = new Laminas\Diactoros\Response();
    $response->getBody()->write('Hello ' . htmlspecialchars($args['name']));
    return $response;
});

$response = $router->dispatch(ServerRequestFactory::fromGlobals());
(new SapiEmitter)->emit($response);

4. Использование замыканий и контейнера зависимостей

Роутер может разрешать обработчики из контейнера, что даёт внедрение зависимостей. Пример с Pimple и собственным роутером:

Пример
<?php
$container = new Pimple\Container();
$container['UserController'] = function ($c) {
    return new UserController($c['db']);
};

// В роутере при обработке используем $container[$handlerName]
// FastRoute поддерживает подстановку строковых имён как callback.
$dispatcher = FastRoute\simpleDispatcher(function($r) use ($container) {
    $r->addRoute('GET', '/users/{id}', 'UserController::show');
});

// В dispatch вызываем: $handler = $routeInfo[1];
list($class, $method) = explode('::', $handler);
$controller = $container[$class];
echo $controller->$method($vars);

5. Обработка ошибок и кастомные 404/405

При использовании FastRoute можно определить собственные замыкания для NotFound и MethodNotAllowed, обернув их в класс.

Пример
<?php
$dispatcher = FastRoute\simpleDispatcher(...);

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        $handler = function() {
            http_response_code(404);
            echo json_encode(['error' => 'Not Found'], JSON_UNESCAPED_UNICODE);
        };
        $handler();
        break;
    // ...
}

Это удобно для API, где нужно возвращать JSON.

Маршрут модуля - comments

En
Php route module (php)