Диспетчеризация запросов: обзор техник маршрутизации

Раздел: PHP фреймворки -> Маршрутизация в PHP

Маршрутизация (dispatch) в PHP позволяет сопоставить входящий HTTP-запрос конкретному обработчику. Без этого механизма невозможно построить гибкое веб-приложение. В материале рассматриваются различные способы реализации роутинга: от простых процедурных конструкций до современных решений на атрибутах.

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

Объектно-ориентированный роутер с поддержкой шаблонов маршрутов

Наиболее эффективное решение для проектов среднего размера. Класс Router хранит зарегистрированные маршруты и при диспетчеризации заменяет плейсхолдеры на регулярные выражения.


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[] = ['method' => $method, 'pattern' => $pattern, 'handler' => $handler];
    }

    public function dispatch(string $method, string $uri): void {
        $uri = parse_url($uri, PHP_URL_PATH);
        foreach ($this->routes as $route) {
            if ($route['method'] !== $method) continue;
            $pattern = preg_replace('#\{([a-zA-Z_]+)\}#', '(?P<$1>[^/]+)', $route['pattern']);
            $pattern = '#^' . $pattern . '$#';
            if (preg_match($pattern, $uri, $matches)) {
                $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
                call_user_func($route['handler'], $params);
                return;
            }
        }
        http_response_code(404);
        echo '404 Not Found';
    }
}

Php dispatch (php dispatch (маршрутизация))

Пояснение шагов: регистрация маршрутов через get/post, преобразование плейсхолдеров {id} в именованные группы, выполнение обработчика с передачей параметров. При несовпадении возвращается 404.

Типичные ошибки: дублирование маршрутов (порядок имеет значение), отсутствие экранирования специальных символов в шаблоне, забытая обработка OPTIONS/HEAD запросов. Рекомендуется добавлять поддержку middleware для логирования и проверки прав.

Как реализовать маршрутизацию в небольшом проекте без фреймворка?

Вариант: простая конструкция switch


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

switch (true) {
    case $uri === '/' && $method === 'GET':
        echo 'Home page';
        break;
    case $uri === '/about' && $method === 'GET':
        echo 'About page';
        break;
    default:
        http_response_code(404);
        echo 'Not Found';
}

Path php category (путь категории в php)

Подходит для страниц с фиксированными URL. Не работает с динамическими параметрами, быстро становится громоздким.

Проблемы: отсутствие поддержки переменных частей, легко забыть break, трудно переиспользовать код. Такой подход оправдан только в самых элементарных сценариях.

Как обрабатывать URL с переменными частями?

Вариант: маршрутизация на регулярных выражениях


if (preg_match('#^/user/(\d+)$#', $uri, $matches)) {
    echo 'User ID: ' . $matches[1];
} elseif (preg_match('#^/post/([a-z0-9-]+)$#', $uri, $matches)) {
    echo 'Post slug: ' . $matches[1];
} else {
    http_response_code(404);
}

Php route product product path (маршрут php для товара)

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

Ошибки: неправильное экранирование слэшей, конфликт маршрутов (например, /user/1 и /user/profile), сложность отладки. Желательно использовать генератор шаблонов.

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

Вариант: библиотека FastRoute (nikic/FastRoute)


use FastRoute\RouteCollector;

$dispatcher = FastRoute\simpleDispatcher(function (RouteCollector $r) {
    $r->addRoute('GET', '/', 'home');
    $r->addRoute('GET', '/user/{id:\d+}', 'user_show');
    $r->addRoute('POST', '/user/{id}/update', 'user_update');
});

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        // вызов обработчика
        break;
    case FastRoute\Dispatcher::NOT_FOUND:
        // 404
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        // 405
        break;
}

FastRoute использует Trie-дерево, что даёт высокую скорость. Поддерживает группировку и именованные параметры.

Проблемы: необходимо подключать через Composer, ограниченная поддержка middleware, сложность интеграции с DI-контейнером без дополнительной обвязки. Рекомендуется кэшировать маршруты в production.

Как организовать роутинг с помощью атрибутов PHP 8?

Вариант: атрибуты и рефлексия


#[Attribute]
class Route {
    public function __construct(public string $path, public string $method = 'GET') {}
}

class UserController {
    #[Route('/user/{id}', method: 'GET')]
    public function show(int $id) {
        echo "User $id";
    }
}

// Сбор маршрутов
$router = new Router();
$refClass = new ReflectionClass(UserController::class);
foreach ($refClass->getMethods() as $method) {
    $attrs = $method->getAttributes(Route::class);
    foreach ($attrs as $attr) {
        $route = $attr->newInstance();
        $router->addRoute($route->method, $route->path, [$refClass->getName(), $method->getName()]);
    }
}

Позволяет держать определение маршрута рядом с обработчиком. Требует сканирования классов при старте приложения.

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

Расширенные примеры и нестандартные подходы

Пример 1. FastRoute с кастомным middleware

Реализация цепочки обработки запроса: логирование + проверка токена + вызов контроллера.

Пример

use FastRoute\RouteCollector;
use Psr\Http\Message\ServerRequestInterface;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

require 'vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(function (RouteCollector $r) {
    $r->addRoute('GET', '/api/user/{id}', 'App\Controller\UserController::show');
});

// PSR-7 Request
$psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$request = $creator->fromGlobals();

$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());

if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) {
    $handler = $routeInfo[1];
    $vars = $routeInfo[2];
    // Middleware: логирование
    error_log('Route matched: ' . $handler);
    // Middleware: проверка токена (упрощённо)
    if (!$request->hasHeader('Authorization')) {
        http_response_code(401);
        echo 'Unauthorized';
        exit;
    }
    // Вызов контроллера
    list($class, $method) = explode('::', $handler);
    $controller = new $class();
    echo $controller->$method($vars);
} else {
    http_response_code(404);
    echo 'Not Found';
}

Результат: при запросе GET /api/user/42 с заголовком Authorization будет вызван UserController::show с параметром id=42.

HTTP/1.1 200 OK
Content-Type: text/html

Данные пользователя с id=42

Пример 2. Кастомный роутер с PSR-7 и инъекцией зависимостей

Роутер самостоятельно создаёт объект контроллера и передаёт нужные параметры из DI-контейнера.

Пример

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response;

class AdvancedRouter {
    private array $routes = [];
    private ContainerInterface $container;

    public function __construct(ContainerInterface $container) {
        $this->container = $container;
    }

    public function add(string $method, string $pattern, array $handler): void {
        $this->routes[] = compact('method', 'pattern', 'handler');
    }

    public function dispatch(ServerRequestInterface $request): ResponseInterface {
        $uri = $request->getUri()->getPath();
        $method = $request->getMethod();
        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);
                list($class, $action) = $route['handler'];
                $controller = $this->container->get($class);
                $response = $controller->$action($request, $params);
                return $response;
            }
        }
        return (new Response())->withStatus(404)->withBody(\Laminas\Diactoros\Stream::create('Not Found'));
    }
}

// Использование
$container = require 'config/container.php';
$router = new AdvancedRouter($container);
$router->add('GET', '/profile/{id}', ['App\Controller\ProfileController', 'show']);
$response = $router->dispatch($request);

Результат: DI-контейнер автоматически передаёт зависимости контроллеру, роутер возвращает PSR-7 Response.

HTTP/1.1 200 OK
Content-Type: application/json
{"id":123,"name":"Иван"}

Пример 3. Маршрутизация через атрибуты с кэшированием

Сбор маршрутов выполняется один раз и сохраняется в файл. При последующих запросах загрузка из кэша.

Пример

class RouteCache {
    private string $cacheFile = __DIR__ . '/route_cache.php';
    private array $routes = [];

    public function build(array $controllers): void {
        foreach ($controllers as $class) {
            $ref = new ReflectionClass($class);
            foreach ($ref->getMethods() as $method) {
                $attrs = $method->getAttributes(\App\Attribute\Route::class);
                foreach ($attrs as $attr) {
                    $route = $attr->newInstance();
                    $this->routes[] = [
                        'method' => $route->method,
                        'path' => $route->path,
                        'handler' => [$class, $method->getName()]
                    ];
                }
            }
        }
        file_put_contents($this->cacheFile, '<? return ' . var_export($this->routes, true) . ';');
    }

    public function load(): array {
        if (file_exists($this->cacheFile)) {
            return require $this->cacheFile;
        }
        return [];
    }
}

// При деплое запускаем build, в рантайме используем load
$cache = new RouteCache();
$cache->build([\App\Controller\UserController::class, \App\Controller\PostController::class]);
// Роутер берёт маршруты из кэша
$routes = $cache->load();

Результат: производительность сохраняется за счёт исключения рефлексии при каждом запросе. Кэш обновляется только при изменении контроллеров.

// Файл route_cache.php (пример содержимого)
array (
  0 => array (
    'method' => 'GET',
    'path' => '/user/{id}',
    'handler' => array (0 => 'App\Controller\UserController', 1 => 'show'),
  ),
)

Пример 4. Роутер с группировкой и генерацией URL по имени

Позволяет задавать префикс для группы и обращаться к маршрутам по псевдониму.

Пример

class NamedRouter {
    private array $namedRoutes = [];
    private string $groupPrefix = '';

    public function group(string $prefix, callable $callback): void {
        $previous = $this->groupPrefix;
        $this->groupPrefix .= $prefix;
        $callback($this);
        $this->groupPrefix = $previous;
    }

    public function add(string $method, string $pattern, $handler, string $name = ''): void {
        $fullPattern = $this->groupPrefix . $pattern;
        $this->routes[] = compact('method', 'fullPattern', 'handler');
        if ($name) {
            $this->namedRoutes[$name] = $fullPattern;
        }
    }

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

// Использование
$router = new NamedRouter();
$router->group('/admin', function (NamedRouter $r) {
    $r->add('GET', '/users', 'admin_users', 'admin.users');
    $r->add('GET', '/user/{id}/edit', 'admin_user_edit', 'admin.user.edit');
});
echo $router->generate('admin.user.edit', ['id' => 5]);

Результат: вывод /admin/user/5/edit. Генерация URL упрощает изменение структуры маршрутов.

/admin/user/5/edit

PHP dispatch (маршрутизация) - comments

En
Php dispatch (php)