kuria/router

HTTP请求路由器

v2.0.0 2019-03-26 20:57 UTC

This package is auto-updated.

Last update: 2024-09-22 17:49:52 UTC


README

HTTP请求路由器。

https://travis-ci.cn/kuria/router.svg?branch=master

内容

功能

  • 使用OO构建器定义路由
  • 匹配请求属性(方法、方案、主机、端口、路径)
  • 正则表达式驱动的主机和路径模式
  • 生成URL

要求

  • PHP 7.1+

使用

路由入站请求

简单的PATH_INFO路由

使用$_SERVER['PATH_INFO']和硬编码的上下文信息进行简单路由。

示例URL:http://localhost/index.php/page/index

<?php

use Kuria\Router\Context;
use Kuria\Router\Result\Match;
use Kuria\Router\Result\MethodNotAllowed;
use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

// create router
$router = new Router();

// define default context
$router->setDefaultContext(new Context(
    'http',         // scheme
    'localhost',    // host
    80,             // port
    '/index.php'    // base path
));

// define routes
$router->defineRoutes(function (RouteCollector $c) {
    $c->get('index')->path('/');
    $c->get('page')->path('/page/{name}');

    $c->addGroup('user_', '/user', function (RouteCollector $c) {
        $c->add('register')->methods(['GET', 'POST'])->path('/register');
        $c->add('login')->methods(['GET', 'POST'])->path('/login');
        $c->get('logout')->path('/logout');
        $c->get('profile')->path('/profile/{username}');
    });
});

// match current request
$path = rawurldecode($_SERVER['PATH_INFO'] ?? '/');
$result = $router->matchPath($_SERVER['REQUEST_METHOD'], $path);

// handle the result
if ($result instanceof Match) {
    // success
    // do something with the matched route and parameters
    echo 'Matched path: ', $result->getSubject()->path, "\n";
    echo 'Matched route: ', $result->getRoute()->getName(), "\n";
    echo 'Parameters: ', print_r($result->getParameters(), true), "\n";
} elseif ($result instanceof MethodNotAllowed) {
    // method not allowed
    http_response_code(405);
    header('Allow: ' . implode(', ', $result->getAllowedMethods()));
    echo "Method not allowed :(\n";
} else {
    // not found
    http_response_code(404);
    echo "Not found :(\n";
}

使用kuria/request-info的动态路由

可以使用kuria/request-info库自动检测上下文和路径信息。

它支持简单的路径信息和重写URL,并可以从受信任的代理头中提取信息。

<?php

use Kuria\RequestInfo\RequestInfo;
use Kuria\Router\Context;
use Kuria\Router\Result\Match;
use Kuria\Router\Result\MethodNotAllowed;
use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

// create router
$router = new Router();

// define default context
$router->setDefaultContext(new Context(
    RequestInfo::getScheme(),
    RequestInfo::getHost(),
    RequestInfo::getPort(),
    RequestInfo::getBasePath()
));

// define routes
$router->defineRoutes(function (RouteCollector $c) {
    $c->get('index')->path('/');
    $c->get('page')->path('/page/{name}');

    $c->addGroup('user_', '/user', function (RouteCollector $c) {
        $c->add('register')->methods(['GET', 'POST'])->path('/register');
        $c->add('login')->methods(['GET', 'POST'])->path('/login');
        $c->get('logout')->path('/logout');
        $c->get('profile')->path('/profile/{username}');
    });
});

// match current request
$path = rawurldecode(RequestInfo::getPathInfo());
$result = $router->matchPath(RequestInfo::getMethod(), $path !== '' ? $path : '/');

// handle the result
if ($result instanceof Match) {
    // success
    // do something with the matched route and parameters
    echo 'Matched path: ', $result->getSubject()->path, "\n";
    echo 'Matched route: ', $result->getRoute()->getName(), "\n";
    echo 'Parameters: ', print_r($result->getParameters(), true), "\n";
} elseif ($result instanceof MethodNotAllowed) {
    // method not allowed
    http_response_code(405);
    header('Allow: ' . implode(', ', $result->getAllowedMethods()));
    echo "Method not allowed :(\n";
} else {
    // not found
    http_response_code(404);
    echo "Not found :(\n";
}

定义路由

RouteCollector提供了一个方便的接口来定义路由。

使用它最简单的方法是使用一个接受RouteCollector实例的回调来调用Router->defineRoutes()。然后路由器负责添加定义的路由。

<?php

use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

$router = new Router();

$router->defineRoutes(function (RouteCollector $c) {
    $c->get('index')->path('/');
    $c->post('login')->path('/login');
    // ...
});

路由收集器API

RouteCollector提供了创建和组织路由构建器的方法。

返回的RouteBuilder实例可以用于配置路由。请参阅路由构建器API

  • add($routeName): RouteBuilder - 添加路由
  • get($routeName): RouteBuilder - 添加匹配GET请求的路由
  • head($routeName): RouteBuilder - 添加匹配HEAD请求的路由
  • post($routeName): RouteBuilder - 添加匹配POST请求的路由
  • put($routeName): RouteBuilder - 添加匹配PUT请求的路由
  • delete($routeName): RouteBuilder - 添加匹配DELETE请求的路由
  • options($routeName): RouteBuilder - 添加匹配OPTIONS请求的路由
  • patch($routeName): RouteBuilder - 添加匹配PATCH请求的路由
  • addVariant($existingRouteName, $newRouteName): RouteBuilder - 添加现有路由的变体,请参阅路由变体
  • addGroup($namePrefix, $pathPrefix, $callback): void - 添加具有公共前缀的一组路由,请参阅路由组
  • hasBuilder($routeName): bool - 检查路由是否已定义
  • getBuilder($routeName): RouteBuilder - 获取给定路由的构建器
  • removeBuilder($routeName): void - 删除路由定义
  • getBuilders(): RouteBuilder[] - 获取所有配置的构建器
  • getRoutes(): Route[] - 构建路由
  • clear(): void - 删除所有定义的路由
路由变体

要添加多个相似的路线,您可以定义单个路由,然后通过调用addVariant()使用该定义作为新路由的基础。

<?php

use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

$router = new Router();

$router->defineRoutes(function (RouteCollector $c) {
    // define a base route
    $c->get('get_row')
        ->path('/{database}/{row}')
        ->defaults(['format' => 'json']);

    // define a variant of the base route
    $c->addVariant('get_row', 'get_row_with_format')
        ->appendPath('.{format}')
        ->requirements(['format' => 'json|xml']);
});

// print defined routes
foreach ($router->getRoutes() as $route) {
    echo $route->getName(), ' :: ', $route->dump(), "\n";
}

输出

get_row :: GET /{database}/{row}
get_row_with_format :: GET /{database}/{row}.{format}
路由组

要定义具有公共路径和名称前缀的多个路由,请使用addGroup()

<?php

use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

$router = new Router();

$router->defineRoutes(function (RouteCollector $c) {
    $c->addGroup('user_', '/user', function (RouteCollector $c) {
        $c->add('register')->methods(['GET', 'POST'])->path('/register');
        $c->add('login')->methods(['GET', 'POST'])->path('/login');
        $c->get('logout')->path('/logout');
        $c->get('profile')->path('/profile/{username}');
    });
});

// print defined routes
foreach ($router->getRoutes() as $route) {
    echo $route->getName(), ' :: ', $route->dump(), "\n";
}

输出

user_register :: GET|POST /user/register
user_login :: GET|POST /user/login
user_logout :: GET /user/logout
user_profile :: GET /user/profile/{username}

路由构建器API

RouteBuilder提供了一个流畅的接口来配置单个路由。

  • methods($allowedMethods): self - 匹配请求方法(必须为大写,例如 GETPOST 等)
  • scheme($scheme): self - 匹配协议(例如 httphttps
  • host($hostPattern): self - 匹配主机名模式,见 路由模式
  • prependHost($hostPatternPrefix): self - 向主机名模式添加前缀
  • appendHost($hostPatternPrefix): self - 向主机名模式添加后缀
  • port($port): self - 匹配端口
  • path($pathPattern): self - 匹配路径模式,见 路由模式
  • prependPath($pathPatternPrefix): self - 向路径模式添加前缀
  • appendPath($pathPatternPrefix): self - 向路径模式添加后缀
  • defaults($defaults): self - 指定默认参数,见 路由默认值
  • attributes($attributes): self - 指定任意路由属性,见 路由属性
  • requirements($requirements): self - 指定参数要求,见 路由要求

示例调用

<?php

$router->defineRoutes(function (RouteCollector $c) {
    // $c->add() returns a RouteBuilder
    $c->add('user_profile_page')
        ->methods(['GET', 'POST'])
        ->scheme('https')
        ->host('{username}.example.com')
        ->port(8080)
        ->path('/{page}')
        ->defaults(['page' => 'home'])
        ->requirements(['username' => '\w+', 'page' => '[\w.\-]+']);
});

路由模式

路由的主机和路径可以包含任意数量的参数占位符。

占位符语法如下

{parameterName}

参数名称可以由任何字符组成,但不能是 }

这些参数将在匹配结果中可用。见 匹配路由

注意

不支持可选模式参数。如果您需要匹配相同资源的不同结构的URL,请相应地定义多个路由。

路由变体

路由默认值

路由可以包含默认参数值。

这些默认值在生成URL时使用(如果未指定一个或多个参数)。见 生成URL

默认参数在定义指向同一资源的多个路由时也很有用(因此这些路由可以互换)。

路由属性

路由可以包含任意属性。

其用途完全取决于应用程序,但它是一个存储各种元数据的好地方,例如控制器名称或处理程序调用。

路由要求

路由要求是针对每个主机或路径模式参数的一组普通正则表达式。见 路由模式

正则表达式不应有分隔符。它们也被自动锚定,因此不应包含 ^$

默认要求

如果没有指定要求,将假定一个默认值,具体取决于模式类型

  • 主机模式:.+
    • 任意类型的多个字符
  • 路径模式:[^/]+
    • 不是斜杠的多个字符

缓存路由

构建和编译路由将向您的应用程序引入一些开销。幸运的是,定义的路由可以序列化并存储以供以后使用。

以下是一个使用 kuria/cache 库进行路由缓存示例,但您可以使用任何其他库或代码。

<?php

use Kuria\Cache\Cache;
use Kuria\Cache\Driver\Filesystem\FilesystemDriver;
use Kuria\Router\Route\RouteCollector;
use Kuria\Router\Router;

// example cache
$cache = new Cache(new FilesystemDriver(__DIR__ . '/cache'));

// create router
$router = new Router();

// attempt to load routes from the cache
$routes = $cache->get('routes');

if ($routes === null) {
    // no routes found in cache, define them
    $router->defineRoutes(function (RouteCollector $c) {
        $c->get('index')->path('/');
        $c->get('page')->path('/page/{name}');
    });

    // store defined routes in the cache
    $cache->set('routes', $router->getRoutes());
} else {
    // use routes from cache
    $router->setRoutes($routes);
}

注意

包含不可序列化值的路由(如属性中的闭包)无法缓存。

匹配路由

定义路由后,可以使用路由器来路由请求。

路由传入请求 中的示例代码。

使用Router->match()/matchPath()

Router->match()Router->matchPath() 都返回 Kuria\Router\Result\Result 的一个实例,该实例可能是以下之一

Kuria\Router\Result\Match

已成功匹配路由。使用Match对象可以访问匹配的路由和参数。

应用程序需要自行处理这些信息。

<?php

use Kuria\Router\Result\Match;
use Kuria\Router\Router;

$result = $router->matchPath('GET', '/user/profile/bob');

if ($result instanceof Match) {
    echo 'Matched route is ', $result->getRoute()->getName(), "\n";
    echo 'Matched parameters are: ', json_encode($result->getParameters()), "\n";
}

输出

Matched route is user_profile
Matched parameters are: {"username":"bob"}

提示

您可以在$result->getRoute()->getAttributes()中访问路由属性。

路由属性

Kuria\Router\Result\MethodNotAllowed

没有匹配到路由,但如果方法不同,则存在匹配的路由。

在这种情况下,正确的响应是HTTP 405方法不允许,带有指定允许方法的Allow头部。

<?php

use Kuria\Router\Result\MethodNotAllowed;

$result = $router->matchPath('POST', '/user/logout');

if ($result instanceof MethodNotAllowed) {
    http_response_code(405);
    header('Allow: ' . implode(', ', $result->getAllowedMethods()));
}
Kuria\Router\Result\NotFound

没有匹配到任何路由。

在这种情况下,正确的响应是HTTP 404未找到。

<?php

use Kuria\Router\Result\NotFound;

$result = $router->matchPath('GET', '/nonexistent');

if ($result instanceof NotFound) {
    http_response_code(404);
}

HEAD到GET回退

为了便于遵守HTTP规范,如果HEAD请求没有匹配任何路由,则将尝试第二次匹配,假设使用GET方法。

PHP本身支持HEAD请求,并且只会响应头部,因此在大多数情况下不需要创建额外的路由来处理这些请求。

生成URL

定义路由后,可以使用路由器生成URL。

路由传入请求,以查看配置的路由示例。

使用Router->generate()

Router->generate()方法将为给定路由和参数生成URL,并返回一个Kuria\Url\Url实例。

  • 如果不存在此类路由或参数无效,将抛出异常(见路由要求
  • 如果缺少某些参数,则将使用配置的默认值(见路由默认值
  • 任何额外的参数(不在主机或路径模式中)将被添加为查询参数
  • 如果方案、主机或端口与上下文不同,则URL的偏好格式为Url::ABSOLUTE;如果它们全部相同或未定义,则将使用Url::RELATIVE(见路由上下文
<?php

var_dump(
    $router->generate('user_register')->build(),
    $router->generate('user_profile', ['username' => 'bob', 'extra' => 'example'])->build()
);

输出

string(14) "/user/register"
string(31) "/user/profile/bob?extra=example"

如果您希望始终获取绝对URL,请使用buildAbsolute()

<?php

var_dump(
    $router->generate('index')->buildAbsolute(),
    $router->generate('page', ['name' => 'contact'])->buildAbsolute()
);

输出

string(17) "http://localhost/"
string(29) "http://localhost/page/contact"

路由器上下文

路由上下文用于在生成URL或匹配路径时填充缺失信息(方案、主机、端口等)。

它可以以两种方式指定

使用Router->setDefaultContext()

此方法定义了一个默认上下文,在未提供时使用。

<?php

use Kuria\Router\Context;

$router->setDefaultContext(new Context(
    'https',       // scheme
    'example.com', // host
    443,           // port
    ''             // basePath
));

使用$context参数

Router->matchPath()Router->generate()接受一个可选的$context参数。

如果没有提供上下文,则将使用默认上下文。如果没有指定默认上下文,则将抛出异常。