volcanus/routing

页面控制器脚本请求-URI路由。

3.0.1 2023-02-03 04:12 UTC

This package is auto-updated.

Last update: 2024-08-30 01:05:54 UTC


README

Latest Stable Version Continuous Integration

这是一个用于通过页面控制器(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。