volcanus / routing
页面控制器脚本请求-URI路由。
Requires
- php: ^8.1
- ext-ctype: *
Requires (Dev)
- phpunit/phpunit: ^9.5
README
这是一个用于通过页面控制器(PageController)模式实现“美丽URI”的库。
在前端控制器(FrontController)模式中,称为Router的类负责解析请求URI并将请求分配给特定的类。
Volcanus_Routing是为了在页面控制器模式中使用的,它解析请求URI,读取特定目录下的脚本文件,并更改当前目录。
通过设置名为参数目录的特殊目录,该库还提供了从请求URI的路径中获取参数的功能。
兼容环境
- PHP 8.1及以上
依赖库
无
简单用法
以下是在Apache + mod_rewrite中使用的一个示例。
/.htaccess
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ __gateway.php [QSA,L]
此外,在Apache 2.2.16及以上版本中,FallbackResource 指令非常方便。
/.htaccess (Apache 2.2.16及以上)
FallbackResource /__gateway.php
如果请求不存在的目录或文件,则将请求转发到以下网关脚本(__gateway.php)。
/__gateway.php
<?php use Volcanus\Routing\Router; use Volcanus\Routing\Exception\NotFoundException; use Volcanus\Routing\Exception\InvalidParameterException; $router = Router::instance([ 'parameterDirectoryName' => '%VAR%', // パラメータディレクトリ名を %VAR% と設定する 'searchExtensions' => 'php', // 読み込み対象スクリプトの拡張子を php と設定する 'overwriteGlobals' => true, // ルーティング実行時、$_SERVERグローバル変数を上書きする ]); $router->importGlobals(); // $_SERVERグローバル変数から環境変数を取り込む try { $router->prepare()->execute(); } catch (\Exception $e) { $text = '500 Internal Server Error'; if ($e instanceof NotFoundException) { $text = '404 Not Found'; } if (!headers_sent() && isset($_SERVER['SERVER_PROTOCOL'])) { header(sprintf('%s %s', $_SERVER['SERVER_PROTOCOL'], $text)); } echo sprintf('<html><head><title>Error %s</title></head><body><h1>%s</h1></body></html>' , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') ); }
当使用上述设置对“/categories/1/items/2/detail.json”这样的请求URI进行路由时,将读取文档根以下“/categories/%VAR%/items/%VAR%/detail.php”的脚本,并更改当前目录。
如果在路由准备过程中未找到要读取的脚本,则会抛出Volcanus\Routing\Exception\NotFoundException异常,因此可以捕获该异常并返回状态404。
在路由执行时,由于设置了overwriteGlobals选项为true,因此$_SERVER全局变量中的PHP_SELF、SCRIPT_NAME、SCRIPT_FILENAME、PATH_INFO、PATH_TRANSLATED将被根据路由结果替换。
/categories/%VAR%/items/%VAR%/detail.php
<?php use Volcanus\Routing\Router; $router = Router::instance(); $categoryId = $router->parameter(0); // '1' $itemId = $router->parameter(1); // '2' $extension = $router->extension(); // 'json'
Router::instance()方法作为Singleton实现,可以从读取的脚本中引用路由结果。
可以通过Router::parameter()方法获取请求路径中参数目录%VAR%对应的段,或者通过Router::extension()方法获取原本请求URI中指定的扩展名。
为了在读取的脚本中使用这些功能,提供了Singleton功能,但并未禁止构造函数的调用,因此可以在路由执行后将Router的实例或parameters()方法的返回值设置为全局变量或类似对象,然后从读取的脚本中引用。
通过分隔符指定参数的类型
从0.2.0版本开始,可以通过指定左右分隔符和类型来获取请求路径的参数。
默认情况下,可以使用包含alpha、digit、alnum、graph等Ctype函数各个关键词的目录名作为参数段。
/__gateway.php
<?php use Volcanus\Routing\Router; use Volcanus\Routing\Exception\NotFoundException; use Volcanus\Routing\Exception\InvalidParameterException; $router = Router::instance([ 'parameterLeftDelimiter' => '{%', // パラメータの左デリミタは {% とする 'parameterRightDelimiter' => '%}', // パラメータの右デリミタは %} とする 'searchExtensions' => 'php', // 読み込み対象スクリプトの拡張子を php と設定する 'overwriteGlobals' => true, // ルーティング実行時、$_SERVERグローバル変数を上書きする ]); $router->importGlobals(); // $_SERVERグローバル変数から環境変数を取り込む try { $router->prepare()->execute(); } catch (\Exception $e) { $text = '500 Internal Server Error'; if ($e instanceof NotFoundException) { $text = '404 Not Found'; } elseif ($e instanceof InvalidParameterException) { $text = '400 Bad Request'; } if (!headers_sent() && isset($_SERVER['SERVER_PROTOCOL'])) { header(sprintf('%s %s', $_SERVER['SERVER_PROTOCOL'], $text)); } echo sprintf('<html><head><title>Error %s</title></head><body><h1>%s</h1></body></html>' , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') );
假设在文档根以下存在“/users/{%digit%}/index.php”这样的脚本...
对于“/users/foo”这样的请求URI的路由,由于InvalidParameterException,将返回状态400。
对于“/users/1”这样的请求URI的路由,将读取相应的脚本。
/users/{%digit%}/index.php
<?php use Volcanus\Routing\Router; $router = Router::instance(); $$user_id = $router->parameter(0); // (string) '1'
通过分隔符指定和自定义过滤器进行参数的验证和转换
通过使用parameterFilters选项,可以定义自定义过滤器来执行不依赖于Ctype函数的参数验证或转换。
/__gateway.php
<?php use Volcanus\Routing\Router; use Volcanus\Routing\Exception\NotFoundException; use Volcanus\Routing\Exception\InvalidParameterException; $router = Router::instance([ 'parameterLeftDelimiter' => '{%', // パラメータの左デリミタは {% とする 'parameterRightDelimiter' => '%}', // パラメータの右デリミタは %} とする 'parameterFilters' => [ // 独自のフィルタ "profile_id" を設定する 'profile_id' => function($value) { if (strspn($value, '0123456789abcdefghijklmnopqrstuvwxyz_-.') !== strlen($value)) { throw new InvalidParameterException('oh...'); } return $value; }, // 標準のフィルタ "digit" を上書き設定する 'digit' => function($value) { if (!ctype_digit($value)) { throw new InvalidParameterException('oh...'); } return intval($value); }, ], 'searchExtensions' => 'php', // 読み込み対象スクリプトの拡張子を php と設定する 'overwriteGlobals' => true, // ルーティング実行時、$_SERVERグローバル変数を上書きする ]); $router->importGlobals(); // $_SERVERグローバル変数から環境変数を取り込む try { $router->prepare()->execute(); } catch (\Exception $e) { $text = '500 Internal Server Error'; if ($e instanceof NotFoundException) { $text = '404 Not Found'; } elseif ($e instanceof InvalidParameterException) { $text = '400 Bad Request'; } if (!headers_sent() && isset($_SERVER['SERVER_PROTOCOL'])) { header(sprintf('%s %s', $_SERVER['SERVER_PROTOCOL'], $text)); } echo sprintf('<html><head><title>Error %s</title></head><body><h1>%s</h1></body></html>' , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') ); }
假设在文档根以下存在“/users/{%digit%}/profiles/{%profile_id%}/index.php”这样的脚本...
对于“/users/1/profiles/invalid@id”这样的请求URI的路由,由于InvalidParameterException,将返回状态400。
对于“/users/1/profiles/k-holy”这样的请求URI的路由,将读取相应的脚本。
/users/{%digit%}/profiles/{%profile_id%}/index.php
<?php use Volcanus\Routing\Router; $router = Router::instance(); $user_id = $router->parameter(0); // (int) 1 $profile_id = $router->parameter(1); // (string) 'k-holy'
通过指定fallbackScript选项,在找不到脚本时读取替代脚本
从0.3.0版本开始,添加了fallbackScript选项,用于在找不到脚本时读取文档根以下任意路径中设置的替代脚本。
/__gateway.php
<?php use Volcanus\Routing\Router; use Volcanus\Routing\Exception\NotFoundException; use Volcanus\Routing\Exception\InvalidParameterException; $router = Router::instance([ 'fallbackScript' => '/path/to/fallback.php', // スクリプトが見つからない場合は ドキュメントルート/path/to/fallback.php を読み込む ]); $router->importGlobals(); // $_SERVERグローバル変数から環境変数を取り込む try { $router->prepare()->execute(); } catch (\Exception $e) { $text = '500 Internal Server Error'; if ($e instanceof NotFoundException) { $text = '404 Not Found'; } if (!headers_sent() && isset($_SERVER['SERVER_PROTOCOL'])) { header(sprintf('%s %s', $_SERVER['SERVER_PROTOCOL'], $text)); } echo sprintf('<html><head><title>Error %s</title></head><body><h1>%s</h1></body></html>' , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') ); }
使用上述设置,例如,如果请求不存在的路径/path/not/found,则将当前目录移动到/path/to并执行fallback.php。
如果通过文件名指定fallbackScript选项,则如果请求的目录中存在该文件,则读取该文件。
/__gateway.php
<?php use Volcanus\Routing\Router; use Volcanus\Routing\Exception\NotFoundException; use Volcanus\Routing\Exception\InvalidParameterException; $router = Router::instance([ 'fallbackScript' => 'fallback.php', // スクリプトが見つからない場合は fallback.php があれば読み込む ]); $router->importGlobals(); // $_SERVERグローバル変数から環境変数を取り込む try { $router->prepare()->execute(); } catch (\Exception $e) { $text = '500 Internal Server Error'; if ($e instanceof NotFoundException) { $text = '404 Not Found'; } if (!headers_sent() && isset($_SERVER['SERVER_PROTOCOL'])) { header(sprintf('%s %s', $_SERVER['SERVER_PROTOCOL'], $text)); } echo sprintf('<html><head><title>Error %s</title></head><body><h1>%s</h1></body></html>' , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') , htmlspecialchars($text, ENT_QUOTES, 'UTF-8') ); }
使用上述设置,例如,如果请求不存在的路径/path/not/found,且/path/not/found.php不存在但/path/not/fallback.php存在,则将当前目录移动到/path/not并执行fallback.php。