Организация маршрутов в PHP: от простых решений до профессиональных инструментов

Раздел: Разработка на PHP -> Маршрутизация

Основные подходы к реализации роутинга в PHP

Как создать собственный роутер с поддержкой динамических параметров и избежать громоздких условных конструкций?

Наиболее эффективное решение для проектов, не использующих фреймворк, - написание компактного класса Router. Такой роутер позволяет логически разделить логику обработки URL и легко добавлять новые маршруты.

<?php
class Router {
    private array $routes = [];

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

    private function convertToRegex(string $pattern): string {
        $pattern = preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $pattern);
        return '#^' . $pattern . '$#';
    }

    public function dispatch(string $uri): void {
        $uri = parse_url($uri, PHP_URL_PATH);
        foreach ($this->routes as $route) {
            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 'Страница не найдена';
    }
}

// Использование
$router = new Router();
$router->add('/user/{id}', function($params) {
    echo "Пользователь ID: " . $params['id'];
});
$router->add('/', function() {
    echo 'Главная страница';
});
$router->dispatch($_SERVER['REQUEST_URI']);
?>

Index php uri (получение uri в index.php)

Пояснение шагов:

  • Метод add() принимает шаблон URL (например, /user/{id}) и обработчик. Шаблон преобразуется в регулярное выражение с именованными группами.
  • Метод dispatch() проходит по всем маршрутам и выполняет первый подходящий. Параметры извлекаются и передаются обработчику.
  • Если маршрут не найден - возвращается 404.

Типичные ошибки и их решение:

  • Проблема: Роутер обрабатывает URL с GET-параметрами неверно.
    Решение: Используйте parse_url($uri, PHP_URL_PATH), чтобы отбросить строку запроса.
  • Проблема: Регулярное выражение ломается при наличии специальных символов в шаблоне.
    Решение: Экранируйте preg_quote() для статических частей шаблона.
  • Проблема: Низкая производительность при большом количестве маршрутов.
    Решение: Используйте кэширование скомпилированных регулярных выражений или библиотеку FastRoute.

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

Как обработать запросы с помощью простого switch без отдельного класса?

Самый простой способ - напрямую анализировать $_SERVER['REQUEST_URI'] в switch:

<?php
$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 '404';
}
?>

Index php page url ru (url страницы index.php на русском)

Такой подход оправдан только для сайтов с 2-3 страницами. При росте числа маршрутов код становится нечитаемым, а добавление динамических параметров (например, /user/42) требует дополнительных проверок.

Проблема:

Нельзя легко изменять или расширять список маршрутов без редактирования основного файла. Решение - использовать массив маршрутов или класс.

Как извлекать параметры из URL с помощью регулярных выражений вручную?

Если нужно поддерживать динамические сегменты без написания полноценного роутера, можно применить preg_match:

<?php
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

$routes = [
    '#^/user/(?P<id>\d+)$#' => function($params) {
        echo 'Пользователь ' . $params['id'];
    },
    '#^/page/(?P<slug>[a-z-]+)$#' => function($params) {
        echo 'Страница ' . $params['slug'];
    },
];

$found = false;
foreach ($routes as $pattern => $handler) {
    if (preg_match($pattern, $uri, $matches)) {
        $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
        $handler($params);
        $found = true;
        break;
    }
}

if (!$found) {
    http_response_code(404);
    echo '404';
}
?>

Search index php route (маршрут поиска в php)

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

Частая ошибка:

Неверное экранирование слэшей в шаблоне. Используйте # как разделитель, чтобы избежать путаницы с /.

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

FastRoute - популярная библиотека, использующая оптимизированное дерево маршрутов. Установка через Composer:

composer require nikic/fast-route

Routes php (роутинг в php)

Пример использования:

<?php
require 'vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addRoute('GET', '/', 'homeHandler');
    $r->addRoute('GET', '/user/{id:\d+}', 'userHandler');
});

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

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        echo call_user_func_array($handler, $vars);
        break;
    case FastRoute\Dispatcher::NOT_FOUND:
        http_response_code(404);
        echo '404';
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        http_response_code(405);
        echo '405 Method Not Allowed';
        break;
}
?>

Библиотека автоматически обрабатывает различные HTTP-методы и предоставляет чёткие сообщения об ошибках.

Типичная проблема:

Несоответствие версий PHP. FastRoute требует PHP 7.4+. Также важно кэшировать скомпилированные маршруты в production.

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

Как интегрировать мощный роутер Symfony Routing без всего фреймворка?

Компонент symfony/routing можно использовать отдельно. Установка:

composer require symfony/routing symfony/config

Пример с определением маршрутов в YAML:

# routes.yaml
home:
    path: /
    controller: App\Controller\HomeController::index
user:
    path: /user/{id}
    controller: App\Controller\UserController::show
    requirements:
        id: '\d+'

PHP-код загрузки и сопоставления:

<?php
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;

$locator = new FileLocator([__DIR__]);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yaml');

$context = new RequestContext($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);
$matcher = new UrlMatcher($collection, $context);

try {
    $parameters = $matcher->match($uri);
    $controller = $parameters['_controller'];
    // Вызвать контроллер...
} catch (ResourceNotFoundException $e) {
    http_response_code(404);
    echo '404';
} catch (MethodNotAllowedException $e) {
    http_response_code(405);
    echo '405';
}
?>

Symfony Routing предоставляет валидацию параметров, генерацию URL, загрузку из разных форматов. Подходит для сложных проектов, где в будущем может понадобиться полный фреймворк.

Проблема:

Избыточность для маленьких проектов. Требуется автозагрузка Composer и знание Dependency Injection.

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

Приведём более сложные сценарии, которые часто встречаются на практике.

Пример 1: Роутер с middleware в собственном классе

Пример
<?php
class RouterWithMiddleware {
    private array $routes = [];
    private array $globalMiddlewares = [];

    public function addMiddleware(callable $middleware): void {
        $this->globalMiddlewares[] = $middleware;
    }

    public function add(string $pattern, array $handlers): void {
        $this->routes[] = [
            'pattern' => $this->convertToRegex($pattern),
            'handlers' => $handlers, // [middleware, ..., controller]
        ];
    }

    private function convertToRegex(string $pattern): string {
        return '#^' . preg_replace('/\{([a-zA-Z_]+)\}/', '(?P<$1>[^/]+)', $pattern) . '$#';
    }

    public function dispatch(string $uri): void {
        $uri = parse_url($uri, PHP_URL_PATH);
        foreach ($this->routes as $route) {
            if (preg_match($route['pattern'], $uri, $matches)) {
                $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
                $handlers = array_merge($this->globalMiddlewares, $route['handlers']);
                $this->runPipeline($handlers, $params);
                return;
            }
        }
        http_response_code(404);
        echo 'Not Found';
    }

    private function runPipeline(array $handlers, array $params): void {
        $pipe = array_reduce(array_reverse($handlers), function($next, $handler) use ($params) {
            return function() use ($handler, $next, $params) {
                return $handler($params, $next);
            };
        }, function() {});
        $pipe();
    }
}

// Использование
$router = new RouterWithMiddleware();
$router->addMiddleware(function($params, $next) {
    echo 'Глобальный middleware выполнен<br>';
    $next();
});
$router->add('/admin/{action}', [
    function($params, $next) {
        // Проверка авторизации
        if (session_status() === PHP_SESSION_NONE) session_start();
        if (empty($_SESSION['user'])) {
            echo 'Доступ запрещён';
            exit;
        }
        $next();
    },
    function($params) {
        echo 'Действие: ' . $params['action'];
    }
]);
$router->dispatch('/admin/create');
?>

Результат выполнения (при авторизованном пользователе):

Глобальный middleware выполнен
Действие: create

Пример 2: Группировка маршрутов с общим префиксом в FastRoute

Пример
<?php
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
    $r->addGroup('/api', function (FastRoute\RouteCollector $r) {
        $r->addRoute('GET', '/users', 'getUsers');
        $r->addRoute('POST', '/users', 'createUser');
        $r->addRoute('GET', '/users/{id:\d+}', 'getUser');
    });
    $r->addGroup('/admin', function (FastRoute\RouteCollector $r) {
        $r->addRoute('GET', '/dashboard', 'adminDashboard');
        $r->addRoute('GET', '/settings', 'adminSettings');
    });
});
// Далее dispatch как в базовом примере
?>

Такой подход упрощает поддержку и читаемость кода.

Пример 3: Кэширование маршрутов FastRoute для production

Пример
<?php
$cacheFile = __DIR__ . '/route_cache.php';

if (file_exists($cacheFile)) {
    $dispatcher = require $cacheFile;
} else {
    $dispatcher = FastRoute\cachedDispatcher(function(FastRoute\RouteCollector $r) {
        $r->addRoute('GET', '/', 'home');
        $r->addRoute('GET', '/news/{slug}', 'news');
    }, [
        'cacheFile' => $cacheFile,
        'cacheDisabled' => false,
    ]);
}
// dispatch as usual
?>

Кэширование ускоряет загрузку маршрутов на 50-70%.

Пример 4: Преобразование параметров (конвертеры) в Symfony Routing

Пример
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();
$route = new Route('/blog/{slug}', [
    '_controller' => 'App\Controller\BlogController::show',
], [
    'slug' => '[a-z0-9-]+'
]);
// Добавим конвертер параметра
$route->setOption('utf8', true);
$route->addDefaults(['slug' => 'default']);
$routes->add('blog_show', $route);

// Или через YAML с опцией requirements
// blog_show:
//     path: /blog/{slug}
//     controller: App\Controller\BlogController::show
//     requirements:
//         slug: '[a-z0-9-]+'

Можно также реализовать собственные конвертеры, реализовав интерфейс ValueResolverInterface.

Пример 5: Генерация URL по имени маршрута (Symfony Router)

Пример
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\RequestContext;

$generator = new UrlGenerator($routes, new RequestContext('/'));
echo $generator->generate('user_show', ['id' => 42]);
// Выведет: /user/42

Это удобно для создания ссылок без жёсткой привязки к структуре URL.

роутинг в PHP - comments

En
Routes php (php)