Маршрутизация
OxPHP маршрутизирует входящие HTTP-запросы с помощью одного из трёх режимов, управляемых единственной переменной окружения. Каждый режим повторяет привычную конфигурацию try_files в nginx, поэтому вы можете точно предсказать, что произойдёт с любым URL.
Как это работает
Каждый запрос проходит через общий конвейер, прежде чем вступит в действие логика конкретного режима:
- Фильтр dot-путей — пути, содержащие скрытые сегменты (
.git,.env), блокируются, за исключением/.well-known/*(RFC 8615) - Поиск в кэше маршрутов — недавно разрешённые URI возвращаются из LRU-кэша (10 000 записей)
- Процентное декодирование + санитизация — закодированные последовательности вроде
%2e%2eдекодируются, а сегменты обхода (..,., пустые) удаляются - Блокировка PHP в well-known — эшелонированная защита:
.php-скрипты внутри/.well-known/никогда не выполняются - Классификация URI — санитизированный путь однократно классифицируется как
NoExtension,PhpилиOtherExtension - Диспетчеризация режима — каждый режим обрабатывает три типа URI по своим правилам
- Проверка символических ссылок — каждый разрешённый путь файловой системы должен канонизироваться внутри корня документов
Шаг классификации — ключ к эффективности: проверка диска для статических ресурсов (/style.css, /logo.png) выполняется один раз в общем слое для URI типа OtherExtension, поэтому все три режима несут одинаковые затраты.
Конфигурация
| Переменная | По умолчанию | Описание |
|---|---|---|
DOCUMENT_ROOT |
/var/www/html/public |
Корневой каталог для раздачи файлов и PHP-скриптов |
ENTRY_FILE |
(не задано) | Единственный канонический входной скрипт. Не задано = Traditional. *.php = Framework. Не-.php = SPA. С WORKER_MODE_ENABLED=true = Worker. Принимает абсолютный путь или путь относительно DOCUMENT_ROOT (.. разрешено); разрешённый путь должен существовать |
WORKER_MODE_ENABLED |
false |
Включает постоянный режим воркеров. Требует, чтобы ENTRY_FILE указывал на .php-скрипт |
Устаревшие переменные INDEX_FILE и WORKER_FILE по-прежнему разбираются (с выводом WARN при запуске) и отображаются на новую модель. См. Конфигурация → Устаревшее.
Режимы маршрутизации
Режимы Traditional, Framework и SPA выбираются с помощью ENTRY_FILE при WORKER_MODE_ENABLED=false, и каждый из них отображается на эквивалентную конфигурацию try_files в nginx.
Активен, когда ENTRY_FILE не задан (или пуст) и WORKER_MODE_ENABLED=false. Эквивалентная конфигурация nginx:
location / {
try_files $uri $uri/ /index.php /index.html =404;
}
location ~ \.php$ {
try_files $uri =404; # PATH_INFO splitting enabled
}Порядок разрешения:
$uri— точный файл на диске → раздать (или выполнить, если.php)$uri/— каталог → искать внутриindex.php, затемindex.html- Разбиение PATH_INFO — когда URI содержит
.php/, префикс скрипта сопоставляется на диске, а остаток становитсяPATH_INFO(например,/api.php/users/42→ скриптapi.php,PATH_INFO=/users/42) /index.php— запасной вариант с корневым front-контроллером/index.html— запасной вариант с корневым статическим индексом- 404
Примеры:
| Запрос | Результат |
|---|---|
/about.php |
Выполнить about.php |
/style.css |
Раздать style.css |
/blog/ (с blog/index.php) |
Выполнить blog/index.php |
/api.php/users/42 |
Выполнить api.php с PATH_INFO=/users/42 |
/missing.txt |
Откатывается к /index.php |
/some/route |
Откатывается к /index.php |
Разбиение PATH_INFO всегда включено в режиме Traditional. Переключателя через переменную окружения нет — прежний флаг SPLIT_PATH_INFO_ENABLED был удалён.
Активен, когда ENTRY_FILE=index.php (или любое значение, заканчивающееся на .php) и WORKER_MODE_ENABLED=false. Эквивалентная конфигурация nginx:
location ~ \.(?!php$)[a-zA-Z0-9]+$ {
try_files $uri /index.php; # static assets: fall back to front controller
}
location / {
rewrite ^ /index.php last; # everything else → front controller
}
location = /index.php {
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass ...;
}Правила разрешения:
| Тип URI | Поведение |
|---|---|
.css, .png, .js, … (любое не-php расширение) |
Раздать файл, если он существует, иначе перезаписать на /index.php |
.php (любой путь) |
Перезаписать на /index.php |
без расширения (/api/users, /) |
Перезаписать на /index.php |
PATH_INFO устанавливается только тогда, когда запрос явно называет входной файл с завершающим сегментом (/index.php/extra); для маршрутов приложения исходный путь читается из REQUEST_URI.
Примеры:
| Запрос | Результат | $_SERVER['PATH_INFO'] |
|---|---|---|
/style.css (существует) |
Раздать style.css |
— |
/style.css (отсутствует) |
Выполнить index.php |
(отсутствует) |
/api/users |
Выполнить index.php |
(отсутствует) |
/about.php |
Выполнить index.php |
(отсутствует) |
/index.php/news/local |
Выполнить index.php |
/news/local |
/index.php (напрямую) |
Выполнить index.php |
(отсутствует) |
/ |
Выполнить index.php |
(отсутствует) |
Для маршрутов приложения исходный путь доступен через REQUEST_URI, поэтому ваш роутер читает $_SERVER['REQUEST_URI'] для диспетчеризации. Прямой доступ к /index.php больше не блокируется — перезапись идемпотентна, поэтому прямое обращение к нему даёт тот же результат, что и обращение к /.
Отсутствующий статический ресурс откатывается к front-контроллеру вместо быстрого возврата 404 — то же поведение try_files $uri /index.php, которое Laravel и Symfony поставляют по умолчанию, поэтому ваше приложение само рендерит свой 404 для отсутствующих ресурсов. Компромисс в том, что каждый запрос к несуществующему ресурсу теперь запускает PHP; если у front-контроллера по-прежнему нечего раздать (сам /index.php отсутствует), запрос возвращает жёсткий 404.
Активен, когда ENTRY_FILE=index.html (или любое не-.php значение) и WORKER_MODE_ENABLED=false. Эквивалентная конфигурация nginx:
location ~ \.php$ {
try_files $uri =404; # PHP: file must exist, no fallback
}
location ~ \. {
try_files $uri =404; # other extensions: hard 404 if missing
}
location / {
try_files /index.html =404; # no-extension paths: straight to index.html
}Правила разрешения:
| Тип URI | Поведение |
|---|---|
.php |
Выполнить файл, если он существует, иначе жёсткий 404 |
.css, .png, … (любое другое расширение) |
Раздать файл, если он существует, иначе жёсткий 404 |
без расширения (/dashboard, /api/users, /) |
Раздать /index.html напрямую — без обращения к диску по $uri |
Примеры:
| Запрос | Результат |
|---|---|
/style.css (существует) |
Раздать style.css |
/style.css (отсутствует) |
404 |
/dashboard |
Раздать /index.html |
/users/42/edit |
Раздать /index.html |
/api.php (существует) |
Выполнить api.php |
/api.php (отсутствует) |
404 |
/index.html (напрямую) |
Раздать index.html |
Два момента семантики, которые стоит отметить:
- Пути без расширения не обращаются к диску — режим SPA никогда не спрашивает «существует ли
/dashboardна диске?». Он всегда возвращает индекс. Это правильно для клиентских роутеров и позволяет избежать лишних вызововstat(). - Отсутствующие статические файлы дают жёсткий 404, а не проваливаются дальше — отсутствующий
/style.cssне отдаёт молчаindex.html. Это позволяет рано выявлять сломанные ссылки на ресурсы вместо возврата HTML там, где JS ожидал CSS.
Поскольку режим SPA выполняет существующие .php-файлы напрямую, применяется PHP_DENY_PATHS — используйте его для блокировки выполнения внутри записываемых каталогов, таких как /uploads.
Режим воркеров
Режим воркеров активируется, когда WORKER_MODE_ENABLED=true и ENTRY_FILE указывает на .php-скрипт. Роутер раздаёт статические ресурсы с диска и направляет все остальные запросы к воркеру ENTRY_FILE — воркер является единственным front-контроллером.
| Тип URI | Поведение |
|---|---|
Статические ресурсы (.css, .png, … — любое не-.php расширение) |
Раздаются напрямую с диска, если присутствуют; отсутствующий ресурс передаётся воркеру ENTRY_FILE (не жёсткий 404) |
Всё остальное (.php-URI, пути без расширения, /) |
Направляется к воркеру ENTRY_FILE |
Произвольные .php-файлы в корне документов никогда не выполняются напрямую в режиме воркеров — запрос к /about.php попадает в колбэк воркера, как и любой другой маршрут, даже если about.php существует на диске. Здесь также нет поиска индекса каталога и нет запасного варианта с корневым index.php; воркер сам видит эти запросы.
Два исключения, оба — защиты уровня сервера, срабатывающие до диспетчеризации режима: пути с dot-сегментами (/.git/config, /.env, голый /.well-known) отклоняются блокировкой dot-путей, а .php-URI внутри /.well-known/ отклоняются в рамках эшелонированной защиты. Оба возвращают 404 и никогда не достигают воркера.
Проверка при запуске отклоняет две комбинации:
WORKER_MODE_ENABLED=trueбезENTRY_FILE→WORKER_MODE_ENABLED=true requires ENTRY_FILE to be set.WORKER_MODE_ENABLED=trueс не-.phpENTRY_FILE→WORKER_MODE_ENABLED=true requires a .php ENTRY_FILE.
Полные детали конфигурации см. в разделе Режим воркеров.
Поведение PATH_INFO
$_SERVER['PATH_INFO'] заполняется по-разному в зависимости от режима:
| Режим | Когда устанавливается | Значение |
|---|---|---|
| Traditional | Только когда URI содержит .php/ (разбиение PATH_INFO) |
Хвост после сегмента скрипта, например /users/42 |
| Framework | Только для явного запроса /index.php/extra |
Хвост после входного файла, например /news |
| SPA | Никогда | (PHP вызывается только для точных .php-файлов; PATH_INFO отсутствует) |
PATH_INFO следует семантике CGI: он присутствует только тогда, когда SCRIPT_NAME (выполняемый скрипт) является буквальным префиксом пути запроса. Перезапись на front-контроллер, которую URL не называет явно — маршрут приложения, индекс каталога, запасной вариант при промахе по статике — не несёт PATH_INFO; вместо этого читайте REQUEST_URI. В режиме Traditional прежняя переменная окружения SPLIT_PATH_INFO_ENABLED была удалена.
Безопасность путей
OxPHP применяет несколько уровней защиты, чтобы предотвратить обход каталогов, раскрытие скрытых файлов и атаки с выходом за пределы через символические ссылки:
- Процентное декодирование выполняется до санитизации, поэтому закодированные попытки обхода вроде
/%2e%2e/etc/passwdперехватываются - Фильтрация сегментов удаляет
..,.и пустые сегменты из разрешённого пути - Проверка символических ссылок канонизирует каждый разрешённый путь и убеждается, что он остаётся внутри корня документов. Символические ссылки, указывающие за пределы раздаваемого каталога, блокируются
- Блокировка dot-путей блокирует любой сегмент пути, начинающийся с
.(например,/.git/config,/.env), за исключением/.well-known/*согласно RFC 8615 - Блокировка PHP в well-known — даже при наличии исключения для dot-путей
.php-скрипты внутри/.well-known/никогда не выполняются (эшелонированная защита) - Дени-лист выполнения PHP — в режимах прямого сопоставления (Traditional и SPA)
PHP_DENY_PATHSблокирует выполнение.phpпо заданным glob-шаблонам (например,/uploads/**или отдельный файл вроде/admin/legacy.php) до любого дискового ввода-вывода. См. Дени-лист выполнения PHP
Если корень документов не существует при запуске, сервер завершается с фатальной ошибкой. Защита от выхода за пределы через символические ссылки требует валидного, разрешимого пути к корню документов.
Устранение неполадок
Все запросы возвращают 404 в режиме Traditional
Убедитесь, что index.php или index.html существует в корне документов. Цепочка try_files режима Traditional откатывается только к ним — если оба отсутствуют и ни один файл не соответствует URL, вы получаете 404.
docker exec <container> ls /var/www/html/publicОтсутствующий статический ресурс возвращает 404 вместо SPA-оболочки
Это намеренное поведение в режиме SPA: отсутствующий /style.css даёт жёсткий 404, а не молчаливый откат к index.html, что позволяет рано выявлять сломанные ссылки на ресурсы. В режимах Framework и Traditional отсутствующий статический файл откатывается к front-контроллеру (/index.php), поэтому роутер вашего приложения рендерит 404. Используйте режим SPA, если хотите жёсткие 404 для отсутствующих ресурсов.
Прямой /index.php больше не возвращает 404
В режиме Framework прямой доступ к front-контроллеру теперь разрешён (перезапись в /index.php идемпотентна). Если раньше вы полагались на 404 для обнаружения прямых обращений, переключитесь на проверку REQUEST_URI изнутри контроллера.
PATH_INFO пуст в режиме Framework
Это ожидаемо для маршрутов приложения. Режим Framework следует семантике CGI: PATH_INFO устанавливается только тогда, когда запрос явно называет входной файл с завершающим сегментом (/index.php/news → /news). Для обычного маршрута приложения вроде /users/42 front-контроллер достигается внутренней перезаписью, которую он не называет, поэтому PATH_INFO отсутствует — читайте путь из $_SERVER['REQUEST_URI']. (Если ENTRY_FILE не заканчивается на .php, OxPHP выбирает режим SPA, который никогда не заполняет PATH_INFO.)
Символическая ссылка внутри корня документов возвращает 404
Символические ссылки, указывающие за пределы корня документов, блокируются намеренно. Переместите целевой контент внутрь корня документов или смонтируйте его как каталог по правильному пути.
Пример для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- ENTRY_FILE=index.phpСм. также
- Статические файлы — определение MIME, кэширование и потоковая передача раздаваемых файлов
- Режим воркеров — постоянные PHP-процессы и маршрутизация в режиме воркеров
- Справочник конфигурации — полный список переменных окружения