Модуль маршрутов PHP: практические подходы и примеры
Введение в маршрутизацию на PHP
Маршрутизация (роутинг) является ключевым компонентом любого веб-приложения. Она определяет, какой код выполняется при запросе определённого URL. В данной статье рассматриваются различные подходы к реализации маршрутизации в PHP: от простых ручных решений до готовых библиотек и PSR-7 совместимых роутеров. Для каждого варианта приведены вопросы, на которые он отвечает, примеры кода, типичные ошибки и способы их исправления.
Основное эффективное решение: FastRoute
FastRoute - легковесная и высокопроизводительная библиотека, используемая в Laravel, Slim и других фреймворках. Она основана на регулярных выражениях и не зависит от дополнительных зависимостей.
Вопрос: Как получить максимальную производительность маршрутизации с минимальным потреблением памяти?
Установка через Composer:
composer require nikic/fast-routePhp 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/configProducts 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 -> listUsers3. 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.