Реализация системы маршрутизации для PHP проектов
Основные подходы к реализации маршрутизации
Маршрутизация (routing) - процесс, при котором входящий URL сопоставляется с конкретным обработчиком (функцией, методом контроллера). В PHP-приложениях, не использующих фреймворк, разработчику приходится самостоятельно организовывать диспетчеризацию. Ниже рассмотрены несколько способов, от простейших до наиболее эффективных.
Эффективный собственный роутер с регулярными выражениями
Как создать гибкую систему маршрутов с поддержкой параметров и HTTP-методов?
Наиболее эффективное решение - написать класс Router, который хранит массив маршрутов и при вызове метода dispatch сопоставляет текущий URI с шаблонами, используя регулярные выражения. Преимущества: полный контроль, высокая производительность, возможность добавить любые дополнительные возможности (фильтры, middleware, генерация URL).
// Пример простого класса Router
class Router {
private array $routes = [];
public function add(string $method, string $pattern, callable $handler): void {
$this->routes[] = [
'method' => $method,
'pattern' => $this->convertPattern($pattern),
'handler' => $handler,
];
}
private function convertPattern(string $pattern): string {
// Замена {param} на именованные группы
return preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $pattern);
}
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;
if (preg_match('#^' . $route['pattern'] . '$#', $uri, $matches)) {
$handler = $route['handler'];
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
call_user_func($handler, $params);
return;
}
}
http_response_code(404);
echo "404 Not Found";
}
}
// Использование:
$router = new Router();
$router->add('GET', '/user/{id}', function($params) {
echo "Пользователь ID: " . $params['id'];
});
$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);App path php (работа с путями файлов в php)
В данном коде метод convertPattern превращает удобный синтаксис {param} в регулярное выражение с именованной группой. При dispatch происходит перебор маршрутов - первый совпавший выполняется. Если ни один не подошёл, возвращается 404.
Типичные проблемы:
- Порядок маршрутов важен: более специфичные должны быть объявлены раньше общих. Иначе более общий шаблон может перехватить запрос.
- Регулярное выражение может быть небезопасным при использовании пользовательских данных (ReDoS). Лучше ограничивать символы в параметрах.
- Отсутствие кэширования: при каждом запросе строится массив маршрутов. Для продакшена стоит кэшировать скомпилированные паттерны.
Простая маршрутизация через switch-case
Как быстро обработать несколько статических URL без создания дополнительных классов?
Можно разместить в index.php блок switch, который анализирует $_SERVER['REQUEST_URI'] и вызывает соответствующий код. Такой подход оправдан для очень маленьких проектов (2–3 страницы), но быстро становится неуправляемым при росте.
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
switch ($uri) {
case '/':
echo 'Главная страница';
break;
case '/about':
echo 'О нас';
break;
case '/contact':
echo 'Контакты';
break;
default:
http_response_code(404);
echo 'Страница не найдена';
}App php domain (работа с доменами в php)
Недостатки:
- Невозможность извлекать параметры из URL (например, /user/123);
- Поддержка только точного совпадения;
- При добавлении новых страниц код становится громоздким;
- Смешивание логики маршрутизации и вывода.
Использование библиотеки FastRoute
Как внедрить мощный и производительный роутер с поддержкой HTTP-методов и групп?
FastRoute от Никиты Попова - популярная библиотека, используемая во многих фреймворках. Устанавливается через Composer: composer require nikic/fast-route. Она предлагает простой синтаксис и высокую скорость благодаря кэшированию.
require 'vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/', 'homeHandler');
$r->addRoute('GET', '/user/{id:\d+}', 'userHandler');
$r->addRoute(['GET', 'POST'], '/contact', 'contactHandler');
});
$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 Not Found';
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo '405 Method Not Allowed';
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
call_user_func($handler, $vars);
break;
}Http user agent php (получение user-agent в php)
Пример обработчика: function userHandler($vars) { echo "User ID: " . $vars['id']; }. FastRoute автоматически преобразует маршруты в регулярные выражения и кэширует их (рекомендуется включить кэш для продакшена).
Частые ошибки:
- Забывают включить автозагрузку Composer;
- Неправильно указывают HTTP-метод (например, POST вместо GET);
- Не обрабатывают случай METHOD_NOT_ALLOWED;
- Порядок маршрутов не важен - FastRoute использует Trie, но группы с разными префиксами могут конфликтовать.
Маршрутизация через атрибуты PHP 8
Как привязать маршрут непосредственно к методу класса, используя атрибуты?
Начиная с PHP 8, можно определять собственные атрибуты (аннотации) и через рефлексию собирать маршруты. Это приближает код к современным фреймворкам, но требует написания парсера.
#[Attribute]
class Route {
public function __construct(public string $method, public string $pattern) {}
}
class UserController {
#[Route('GET', '/user/{id}')]
public function show(int $id) {
echo "User $id";
}
}
// Сбор маршрутов:
$routes = [];
$refClass = new ReflectionClass(UserController::class);
foreach ($refClass->getMethods() as $method) {
$attrs = $method->getAttributes(Route::class);
foreach ($attrs as $attr) {
$route = $attr->newInstance();
$routes[] = [
'method' => $route->method,
'pattern' => $route->pattern,
'handler' => [$refClass->getName(), $method->getName()],
];
}
}
// Далее обработка аналогична роутеру из первого варианта.Config app php (конфигурация php приложения)
Для продакшена необходимо кэшировать список маршрутов, чтобы не выполнять рефлексию при каждом запросе.
Проблемы:
- Зависимость от PHP 8+;
- Рефлексия может замедлять работу без кэширования;
- Необходимо самостоятельно обрабатывать внедрение зависимостей.
Микрофреймворк Slim
Как получить готовую инфраструктуру для REST API с минимальными усилиями?
Slim - микрофреймворк, предоставляющий маршрутизацию, middleware и контейнер зависимостей. Устанавливается через Composer: composer require slim/slim. Пример:
require 'vendor/autoload.php';
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->get('/', function ($request, $response) {
$response->getBody()->write('Hello World');
return $response;
});
$app->get('/user/{id}', function ($request, $response, $args) {
$id = $args['id'];
$response->getBody()->write("User $id");
return $response;
});
$app->run();Slim берёт на себя обработку HTTP-методов, генерацию ответов и middleware.
Недостатки:
- Привязка к фреймворку;
- Для простых сайтов может быть избыточным;
- Требуется понимание PSR-7 и PSR-15.
Расширенные примеры маршрутизации
В этом разделе приведены более детальные реализации каждого подхода с полным кодом и результатом работы.
Пример 1: Полноценный роутер с поддержкой именованных маршрутов и генерации URL
Расширим класс Router, добавив возможность давать маршрутам имена и генерировать по ним URL. Это полезно для шаблонов.
class Router {
private array $routes = [];
private array $namedRoutes = [];
public function add(string $method, string $pattern, callable $handler, ?string $name = null): void {
$compiled = $this->compilePattern($pattern);
$this->routes[] = [
'method' => $method,
'pattern' => $compiled,
'handler' => $handler,
'name' => $name,
];
if ($name) {
$this->namedRoutes[$name] = $pattern;
}
}
private function compilePattern(string $pattern): string {
// поддержка {param} и {param:regex}
return preg_replace_callback('/\{([a-zA-Z_]+)(?::([^}]+))?\}/', function($match) {
$name = $match[1];
$regex = $match[2] ?? '[^/]+';
return '(?P<' . $name . '>' . $regex . ')';
}, $pattern);
}
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;
if (preg_match('#^' . $route['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";
}
public function generateUrl(string $name, array $params = []): string {
if (!isset($this->namedRoutes[$name])) {
throw new Exception("Route '$name' not found.");
}
$pattern = $this->namedRoutes[$name];
foreach ($params as $key => $value) {
$pattern = str_replace('{' . $key . '}', $value, $pattern);
}
// Удаление оставшихся параметров (если не все заменены)
return preg_replace('/\{[a-zA-Z_]+\}/', '', $pattern);
}
}
// Использование:
$router = new Router();
$router->add('GET', '/user/{id:\d+}', function($params) {
echo "Просмотр пользователя ID: " . $params['id'];
}, 'user.show');
$router->add('GET', '/user/{id}/post/{postId}', function($params) {
echo "Пост $postId пользователя $id";
}, 'user.post');
// Генерация URL:
echo $router->generateUrl('user.show', ['id' => 42]); // /user/42
echo $router->generateUrl('user.post', ['id' => 1, 'postId' => 10]); // /user/1/post/10/user/42 /user/1/post/10
Пример 2: Использование FastRoute с группами и middleware (через функцию-обёртку)
FastRoute поддерживает группы маршрутов с общим префиксом. Также можно встроить простую middleware-проверку.
require 'vendor/autoload.php';
use FastRoute\RouteCollector;
use FastRoute\Dispatcher;
$dispatcher = FastRoute\simpleDispatcher(function(RouteCollector $r) {
$r->addGroup('/admin', function(RouteCollector $r) {
$r->addRoute('GET', '', 'adminDashboard');
$r->addRoute('GET', '/users', 'adminUsers');
});
$r->addRoute('GET', '/api/user/{id:\d+}', 'apiGetUser');
});
// Функция middleware для проверки аутентификации (упрощённо)
function authMiddleware(callable $next) {
return function(...$args) use ($next) {
session_start();
if (!isset($_SESSION['user'])) {
http_response_code(401);
echo "Unauthorized";
return;
}
call_user_func($next, ...$args);
};
}
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
http_response_code(404);
echo "404";
break;
case Dispatcher::METHOD_NOT_ALLOWED:
http_response_code(405);
echo "405";
break;
case Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// Применение middleware
if ($handler === 'adminDashboard' || $handler === 'adminUsers') {
$handler = authMiddleware($handler);
}
call_user_func($handler, $vars);
break;
}Результат: при запросе /admin без сессии вернёт 401, с сессией - вызовет обработчик.
Пример 3: Кэширование маршрутов, полученных через атрибуты
Чтобы избежать рефлексии при каждом запросе, можно сохранить маршруты в файл и загружать их.
// cache.php - генерация кэша
function buildRouteCache(): array {
$routes = [];
$controllers = [UserController::class, PostController::class];
foreach ($controllers as $class) {
$refClass = new ReflectionClass($class);
foreach ($refClass->getMethods() as $method) {
$attrs = $method->getAttributes(Route::class);
foreach ($attrs as $attr) {
$route = $attr->newInstance();
$routes[] = [
'method' => $route->method,
'pattern' => $route->pattern,
'handler' => [$class, $method->getName()],
];
}
}
}
file_put_contents('routes_cache.php', '<?php return ' . var_export($routes, true) . ';');
return $routes;
}
// В index.php:
if (file_exists('routes_cache.php')) {
$routes = include 'routes_cache.php';
} else {
$routes = buildRouteCache();
}
// Далее используем $routes для dispatch.При изменении контроллеров нужно очищать кэш (например, при деплое).
Пример 4: Slim с контейнером зависимостей и middleware
Slim легко интегрируется с PSR-11 контейнером. Пример с использованием PHP-DI.
require 'vendor/autoload.php';
use Slim\Factory\AppFactory;
use DI\ContainerBuilder;
$containerBuilder = new ContainerBuilder();
$container = $containerBuilder->build();
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->addErrorMiddleware(true, true, true);
// Middleware для логирования
$app->add(function ($request, $handler) {
$response = $handler->handle($request);
error_log('Request: ' . $request->getMethod() . ' ' . $request->getUri());
return $response;
});
$app->get('/hello/{name}', function ($request, $response, $args) {
$name = $args['name'];
$response->getBody()->write("Hello, $name!");
return $response;
});
$app->run();Результат: при GET /hello/World выведет "Hello, World!" и запишет лог.