ルーティング
OxPHP は、単一の環境変数で制御される3つのモードのいずれかを使って、受信した HTTP リクエストをルーティングします。各モードは馴染みのある nginx の try_files 設定を再現しているため、どの URL に対しても何が起こるかを正確に予測できます。
仕組み
すべてのリクエストは、モード固有のロジックが動き出す前に、共通のパイプラインを通過します。
- ドットパスフィルター — 隠しセグメント(
.git、.env)を含むパスはブロックされます。ただし/.well-known/*は例外です(RFC 8615) - ルートキャッシュの検索 — 最近解決された URI は LRU キャッシュ(10 000 エントリ)から返されます
- パーセントデコード + サニタイズ —
%2e%2eのようなエンコードされたシーケンスがデコードされ、トラバーサルセグメント(..、.、空)が除去されます - well-known 内の PHP ブロック — 多層防御として、
/.well-known/内の.phpスクリプトは決して実行されません - URI の分類 — サニタイズされたパスは、一度だけ
NoExtension、Php、OtherExtensionのいずれかに分類されます - モードディスパッチ — 各モードは、それぞれ独自のルールで3種類の URI を処理します
- シンボリックリンクの検証 — 解決されたすべてのファイルシステムパスは、ドキュメントルート内に正規化されなければなりません
分類ステップが効率化の鍵です。静的アセット(/style.css、/logo.png)のディスクチェックは、OtherExtension の URI に対して共通レイヤーで一度だけ実行されるため、3つのモードすべてが同じコストで済みます。
設定
| 変数 | デフォルト | 説明 |
|---|---|---|
DOCUMENT_ROOT |
/var/www/html/public |
ファイルと PHP スクリプトを提供するルートディレクトリ |
ENTRY_FILE |
(未設定) | 単一の正規エントリースクリプト。未設定 = 従来型。*.php = フレームワーク。非 .php = SPA。WORKER_MODE_ENABLED=true を併用 = ワーカー。絶対パス、または DOCUMENT_ROOT からの相対パス(.. 可)を受け付けます。解決されたパスは存在しなければなりません |
WORKER_MODE_ENABLED |
false |
永続的なワーカーモードを有効にします。ENTRY_FILE が .php スクリプトを指している必要があります |
レガシーの INDEX_FILE と WORKER_FILE 変数は今も解析され(起動時に WARN を出します)、新しいモデルへマッピングされます。設定 → 非推奨を参照してください。
ルーティングモード
従来型、フレームワーク、SPA の各モードは、WORKER_MODE_ENABLED=false の状態で ENTRY_FILE によって選択され、それぞれが同等の nginx try_files 設定にマッピングされます。
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— ルートのフロントコントローラーへのフォールバック/index.html— ルートの静的インデックスへのフォールバック- 404
例:
| リクエスト | 結果 |
|---|---|
/about.php |
about.php を実行 |
/style.css |
style.css を提供 |
/blog/(blog/index.php あり) |
blog/index.php を実行 |
/api.php/users/42 |
PATH_INFO=/users/42 で api.php を実行 |
/missing.txt |
/index.php にフォールバック |
/some/route |
/index.php にフォールバック |
PATH_INFO 分割は従来型モードでは常に有効です。環境変数のスイッチはありません。以前の 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 への直接アクセスはもはやブロックされません。リライトは冪等なので、直接アクセスしても / にアクセスしたのと同じ結果になります。
不在の静的アセットは、即座に 404 を返すのではなくフロントコントローラーにフォールバックします。これは Laravel や Symfony がデフォルトで搭載している try_files $uri /index.php と同じ動作で、不在のアセットに対してはアプリケーション自身が独自の 404 をレンダリングします。トレードオフとして、存在しないアセットへのすべてのリクエストが今や PHP を実行することになります。フロントコントローラーにも提供するものがなければ(/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 を提供 |
注目に値するセマンティクスが2つあります。
- 拡張子なしのパスはディスクをスキップします — SPA モードは「
/dashboardはディスク上に存在するか?」を決して問い合わせません。常にインデックスを返します。これはクライアントサイドのルーターにとって正しい動作であり、不要なstat()呼び出しを回避します。 - 不在の静的ファイルはフォールスルーではなくハードな 404 になります — 不在の
/style.cssが黙ってindex.htmlを提供することはありません。これにより、JS が CSS を期待している箇所で HTML を返す代わりに、壊れたアセット参照を早期に検出できます。
SPA モードは既存の .php ファイルを直接実行するため、PHP_DENY_PATHS が適用されます。/uploads のような書き込み可能なディレクトリ内での実行をブロックするために使用してください。
ワーカーモード
ワーカーモードは、WORKER_MODE_ENABLED=true かつ ENTRY_FILE が .php スクリプトを指しているときに有効になります。ルーターは静的アセットをディスクから提供し、それ以外のすべてのリクエストをワーカーの ENTRY_FILE にディスパッチします。ワーカーが唯一のフロントコントローラーです。
| URI の種類 | 動作 |
|---|---|
静的アセット(.css、.png、… — .php 以外の任意の拡張子) |
存在すればディスクから直接提供。不在のアセットはワーカーの ENTRY_FILE にフォールスルー(ハードな 404 ではありません) |
それ以外すべて(.php の URI、拡張子なしのパス、/) |
ワーカーの ENTRY_FILE にディスパッチ |
ワーカーモードでは、ドキュメントルート内の任意の .php ファイルが直接実行されることは決してありません。/about.php へのリクエストは、たとえ about.php がディスク上に存在していても、他のルートと同様にワーカーのコールバックに到達します。ディレクトリインデックスの検索もなく、ルートの index.php フォールバックもありません。ワーカー自身がそれらのリクエストを受け取ります。
例外が2つあり、いずれもモードディスパッチの前に動作するサーバーレベルの防御です。ドットセグメントのパス(/.git/config、/.env、裸の /.well-known)はドットパスブロッキングによって拒否され、/.well-known/ 下の .php の URI は多層防御として拒否されます。どちらも 404 を返し、ワーカーに到達することはありません。
起動時の検証では、次の2つの組み合わせが拒否されます。
ENTRY_FILEなしのWORKER_MODE_ENABLED=true→WORKER_MODE_ENABLED=true requires ENTRY_FILE to be set。.php以外のENTRY_FILEを伴うWORKER_MODE_ENABLED=true→WORKER_MODE_ENABLED=true requires a .php ENTRY_FILE。
完全な設定の詳細についてはワーカーモードを参照してください。
PATH_INFO の動作
$_SERVER['PATH_INFO'] は、モードによって異なる方法で設定されます。
| モード | 設定されるとき | 値 |
|---|---|---|
| 従来型 | URI に .php/ が含まれるときのみ(PATH_INFO 分割) |
スクリプトセグメントの後の末尾部分、例: /users/42 |
| フレームワーク | 明示的な /index.php/extra リクエストのときのみ |
エントリーファイルの後の末尾部分、例: /news |
| SPA | 決して設定されない | (PHP は正確な .php ファイルに対してのみ呼び出されるため、PATH_INFO はありません) |
PATH_INFO は CGI のセマンティクスに従います。実行されたスクリプトである SCRIPT_NAME がリクエストパスのリテラルなプレフィックスである場合にのみ存在します。URL が指名しないフロントコントローラーのリライト(アプリケーションのルート、ディレクトリインデックス、静的ミスのフォールバック)は PATH_INFO を持ちません。代わりに REQUEST_URI を読み取ってください。従来型モードでは、以前の SPLIT_PATH_INFO_ENABLED 環境変数は削除されました。
パスのセキュリティ
OxPHP は、ディレクトリトラバーサル、隠しファイルの開示、シンボリックリンクによるエスケープ攻撃を防ぐために、複数の保護レイヤーを適用します。
- パーセントデコードはサニタイズの前に実行されるため、
/%2e%2e/etc/passwdのようなエンコードされたトラバーサルの試みが捕捉されます - セグメントフィルタリングは、解決されたパスから
..、.、空のセグメントを除去します - シンボリックリンクの検証は、解決されたすべてのパスを正規化し、それがドキュメントルート内に留まっていることを確認します。提供対象のディレクトリの外を指すシンボリックリンクはブロックされます
- ドットパスブロッキングは、
.で始まる任意のパスセグメント(例:/.git/config、/.env)をブロックします。ただし RFC 8615 に従い/.well-known/*は例外です - well-known 内の PHP ブロック — ドットパスの例外があっても、
/.well-known/下の.phpスクリプトは決して実行されません(多層防御) - PHP 実行の拒否リスト — 直接マッピングモード(従来型と SPA)では、
PHP_DENY_PATHSが、設定された glob パターン(例:/uploads/**、または/admin/legacy.phpのような単一ファイル)における.phpの実行を、ディスク I/O の前にブロックします。PHP 実行の拒否リストを参照してください
起動時にドキュメントルートのディレクトリが存在しない場合、サーバーは致命的なエラーで終了します。シンボリックリンクのエスケープ保護には、有効で解決可能なドキュメントルートのパスが必要です。
トラブルシューティング
従来型モードですべてのリクエストが 404 を返す
ドキュメントルートに index.php または index.html が存在することを確認してください。従来型モードの try_files チェーンはこれらにのみフォールバックします。両方が不在で、かつ URL にマッチするファイルがない場合、404 が返されます。
docker exec <container> ls /var/www/html/public不在の静的アセットが SPA シェルではなく 404 を返す
これは SPA モードでは意図的な動作です。不在の /style.css は、index.html への黙ったフォールバックではなくハードな 404 となり、壊れたアセット参照を早期に検出できます。フレームワークモードと従来型モードでは、不在の静的ファイルはフロントコントローラー(/index.php)にフォールバックするため、アプリケーションのルーターが 404 をレンダリングします。不在のアセットに対してハードな 404 が欲しい場合は SPA モードを使用してください。
直接の /index.php がもう 404 を返さない
フレームワークモードでは、フロントコントローラーへの直接アクセスが許可されるようになりました(/index.php へのリライトは冪等です)。以前、直接アクセスを検出するために 404 に依存していた場合は、コントローラー内から REQUEST_URI をチェックする方法に切り替えてください。
フレームワークモードで PATH_INFO が空
これはアプリケーションのルートでは想定どおりの動作です。フレームワークモードは CGI のセマンティクスに従います。PATH_INFO は、リクエストが末尾セグメント付きでエントリーファイルを明示的に指名した場合(/index.php/news → /news)にのみ設定されます。/users/42 のような通常のアプリルートでは、フロントコントローラーは指名しない内部リライトによって到達されるため、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