Реализация системы маршрутизации для PHP проектов

Раздел: PHP -> Веб-разработка на 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.
- Php create html (создание html в php)
- Default php app (настройки по умолчанию в php приложении)
- Php веб сервисы (php веб-сервисы)

Расширенные примеры маршрутизации

В этом разделе приведены более детальные реализации каждого подхода с полным кодом и результатом работы.

Пример 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!" и запишет лог.

Маршрутизация в PHP приложении - comments

En
App php route (php)