Диспетчеризация запросов: обзор техник маршрутизации
Маршрутизация (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