crysalead/router

HTTP 请求路由器

3.1.0 2019-11-17 20:31 UTC

This package is auto-updated.

Last update: 2024-09-17 23:15:15 UTC


README

Build Status Code Coverage

完整的基准测试结果可在此处找到。

  • 兼容 PSR-7
  • 命名路由
  • 反向路由
  • 子域
  • 嵌套路由
  • 自定义调度策略
  • 高级路由模式语法

安装

composer require crysalead/router

API

路由模式

路由模式是路径字符串,包含花括号占位符。可能的占位符格式有:

  • '{name}' - 占位符
  • '{name:regex}' - 带正则定义的占位符。
  • '[{name}]' - 可选占位符
  • '[{name}]+' - 可重复占位符
  • '[{name}]*' - 可选可重复占位符

变量占位符可能只包含单词字符(拉丁字母、数字和下划线),并且必须在模式中唯一。对于没有显式正则表达式的变量占位符,它匹配除 '/' 之外的任意数量的字符(即 [^/]+)。

您可以使用方括号(即 [])使模式的部分可选。例如,/foo[bar] 将匹配 /foo/foobar。可选部分可以使用 []*[]+ 语法嵌套和重复。例如:/{controller}[/{action}[/{args}]*]

示例

  • '/foo/' - 仅当路径正好为 '/foo/' 时匹配。没有对尾部斜杠的特殊处理,并且模式必须匹配整个路径,而不仅仅是前缀。
  • '/user/{id}' - 匹配 '/user/bob' 或 '/user/1234!!!' 或甚至是 '/user/bob/details',但不匹配 '/user/' 或 '/user'。
  • '/user/{id:[^/]+}' - 与前面的示例相同。
  • '/user[/{id}]' - 与前面的示例相同,但也匹配 '/user'。
  • '/user[/[{id}]]' - 与前面的示例相同,但也匹配 '/user/'。
  • '/user[/{id}]*' - 匹配 '/user' 以及 'user/12/34/56'。
  • '/user/{id:[0-9a-fA-F]{1,8}}' - 仅当 id 参数由 1 到 8 个十六进制数字组成时匹配。
  • '/files/{path:.*}' - 匹配以 '/files/' 开头的任何 URL,并将路径的其余部分捕获到参数 'path' 中。

注意:例如,/{controller}[/{action}[/{args}]*]/{controller}[/{action}[/{args:.*}]] 之间的区别是使用 [/{args}]*args 将是一个数组,而使用 [/{args:.*}] 时将是一个唯一的 "斜线" 字符串。

路由器

可以实例化 Router 实例

use Lead\Router\Router;

$router = new Router();

可选地,如果您的项目位于 Web 根目录的子文件夹中,您需要使用 basePath() 设置基本路径。此基本路径将被忽略,因此您的路由不需要以它为前缀即可匹配请求路径。

$router->basePath('/my/sub/dir');

注意:如果您正在使用 crysalead/net 库,您可以直接传递 Request::ingoing()->basePath();,因此您不需要手动设置它。

路由器公共方法

$router->basePath();   // Gets/sets the router base path
$router->group();      // To create some scoped routes
$router->bind();       // To create a route
$router->route();      // To route a request
$router->link();       // To generate a route's link
$router->apply();      // To add a global middleware
$router->middleware(); // The router's middleware generator
$router->strategy();   // Gets/sets a routing strategy

路由定义

路由定义示例

use Lead\Router\Router;

$router = new Router();

$router->bind($pattern, $handler);                                 // route matching any request method
$router->bind($pattern, $options, $handler);                       // alternative syntax with some options.
$router->bind($pattern, ['methods' => 'GET'], $handler);           // route matching on only GET requests
$router->bind($pattern, ['methods' => ['POST', 'PUT']], $handler); // route matching on POST and PUT requests

// Alternative syntax
$router->get($pattern, $handler);    // route matching only get requests
$router->post($pattern, $handler);   // route matching only post requests
$router->delete($pattern, $handler); // route matching only delete requests

在上面的示例中,使用 ->bind() 方法注册了一个路由,并作为参数传递了路由模式、可选的选项数组和回调处理程序。

第二个参数是一个 $options 数组,其中可能的值包括:

  • 'scheme': 方案约束(默认:'*'
  • 'host': 主机约束(默认:'*'
  • 'methods': 方法约束(默认:'*'
  • 'name': 路由的名称(可选)
  • 'namespace': 要附加到路由的命名空间(可选)

最后一个参数是回调处理器,它包含当路由与请求匹配时执行的分发逻辑。回调处理器以匹配的路由作为第一个参数,响应对象作为第二个参数被调用。

$router->bind('foo/bar', function($route, $response) {
});

路由公共属性

$route->method;       // The method contraint
$route->params;       // The matched params
$route->persist;      // The persisted params
$route->namespace;    // The namespace
$route->name;         // The route's name
$route->request;      // The routed request
$route->response;     // The response (same as 2nd argument, can be `null`)
$route->dispatched;   // To store the dispated instance if applicable.

路由公共方法

$route->host();       // The route's host instance
$route->pattern();    // The pattern
$route->regex();      // The regex
$route->variables();  // The variables
$route->token();      // The route's pattern token structure
$route->scope();      // The route's scope
$route->error();      // The route's error number
$route->message();    // The route's error message
$route->link();       // The route's link
$route->apply();      // To add a new middleware
$route->middleware(); // The route's middleware generator
$route->handler();    // The route's handler
$route->dispatch();   // To dispatch the route (i.e execute the route's handler)

命名路由和反向路由

要能够进行一些反向路由,首先必须使用以下语法对路由进行命名:

$route = $router->bind('foo/{bar}', ['name' => 'foo'], function() { return 'hello'; });

可以使用数组语法在路由器实例中检索命名路由。

$router['foo']; // Returns the `'foo'` route.

一旦命名,可以使用 ->link() 方法进行反向路由。

echo $router->link('foo', ['bar' => 'baz']); // /foo/baz

->link() 方法将路由名称作为第一个参数,将路由参数作为第二个参数。

分组路由

通过使用 ->group() 方法将路由分组到一个专用组中,可以一次性将作用域应用于一组路由。

$router->group('admin', ['namespace' => 'App\Admin\Controller'], function($router) {
    $router->bind('{controller}[/{action}]', function($route, $response) {
        $controller = $route->namespace . ucfirst($route->params['controller']);
        $instance = new $controller($route->params, $route->request, $route->response);
        $action = isset($route->params['action']) ? $route->params['action'] : 'index';
        $instance->{$action}();
        return $route->response;
    });
});

上面的示例将能够将 /admin/user/edit 路由到 App\Admin\Controller\User::edit()。控制器完全限定类名使用 {controller} 变量构建,然后通过运行 {action} 方法实例化以处理请求。

子域和/或前缀路由

为了支持一些子域路由,最简单的方法是使用 ->group() 方法分组路由,并设置主机约束如下:

$router->group(['host' => 'foo.{domain}.bar'], function($router) {
    $router->group('admin', function($router) {
        $router->bind('{controller}[/{action}]', function() {});
    });
});

例如,上述示例将能够路由 http://foo.hello.bar/admin/user/edit

中间件

中间件函数是可以访问请求对象、响应对象以及应用请求-响应周期中下一个中间件的函数。中间件函数提供了与面向方面编程(AOP)中的方面相同级别的控制。它允许:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。

还可以在单个路由或一组路由上全局应用中间件函数。向路由添加中间件使用 ->apply() 方法。

$mw = function ($request, $response, $next) {
    return 'BEFORE' . $next($request, $response) . 'AFTER';
};


$router->get('foo', function($route) {
    return '-FOO-';
})

echo $router->route('foo')->dispatch($response); //BEFORE-FOO-AFTER

也可以在分组上附加中间件。

$mw1 = function ($request, $response, $next) {
    return '1' . $next($request, $response) . '1';
};
$mw2 = function ($request, $response, $next) {
    return '2' . $next($request, $response) . '2';
};
$mw3 = function ($request, $response, $next) {
    return '3' . $next($request, $response) . '3';
};
$router->apply($mw1); // Global

$router->group('foo', function($router) {
    $router->get('bar', function($route) {
        return '-BAR-';
    })->apply($mw3);  // Local
})->apply($mw2);      // Group

echo $router->route('foo/bar')->dispatch($response); //321-BAR-123

分发

分发是框架的最外层,负责接收初始HTTP请求并在请求生命周期结束时发送响应。

这一步骤有责任加载和实例化正确的控制器、资源或类来构建响应。由于所有这些逻辑都取决于应用架构,因此分发被拆分为两个步骤,以尽可能地保持灵活性。

分发请求

URL分发分为两个步骤。首先在路由器实例上调用 ->route() 方法来查找与URL匹配的路由。路由接受以下参数

  • Psr\Http\Message\RequestInterface 的实例
  • 一个URL或路径字符串
  • 包含至少一个路径条目的数组
  • 以下顺序的参数列表:路径、方法、主机和方案

->route() 方法返回一个路由(或“未找到”路由),然后 ->dispatch() 方法将执行路由处理器中的分发逻辑(或对于无效路由抛出异常)。

use Lead\Router\Router;

$router = new Router();

// Bind to all methods
$router->bind('foo/bar', function() {
    return "Hello World!";
});

// Bind to POST and PUT at dev.example.com only
$router->bind('foo/bar/edit', ['methods' => ['POST',' PUT'], 'host' => 'dev.example.com'], function() {
    return "Hello World!!";
});

// The Router class makes no assumption of the ingoing request, so you have to pass
// uri, methods, host, and protocol into `->route()` or use a PSR-7 Compatible Request.
// Do not rely on $_SERVER, you must check or sanitize it!
$route = $router->route(
    $_SERVER['REQUEST_URI'], // foo/bar
    $_SERVER['REQUEST_METHOD'], // get, post, put...etc
    $_SERVER['HTTP_HOST'], // www.example.com
    $_SERVER['SERVER_PROTOCOL'] // http or https
);

echo $route->dispatch(); // Can throw an exception if the route is not valid.

使用PSR-7兼容的请求/响应实例分发请求

还可以使用兼容的请求/响应实例进行分发。

use Lead\Router\Router;
use Lead\Net\Http\Cgi\Request;
use Lead\Net\Http\Response;

$request = Request::ingoing();
$response = new Response();

$router = new Router();
$router->bind('foo/bar', function($route, $response) {
    $response->body("Hello World!");
    return $response;
});

$route = $router->route($request);

echo $route->dispatch($response); // Can throw an exception if the route is not valid.

处理分发失败

use Lead\Router\RouterException;
use Lead\Router\Router;
use Lead\Net\Http\Cgi\Request;
use Lead\Net\Http\Response;

$request = Request::ingoing();
$response = new Response();

$router = new Router();
$router->bind('foo/bar', function($route, $response) {
    $response->body("Hello World!");
    return $response;
});

$route = $router->route($request);

try {
    echo $route->dispatch($response);
} catch (RouterException $e) {
    http_response_code($e->getCode());
    // Or you can use Whoops or whatever to render something
}

设置自定义分发策略。

要使用自己的策略,您需要使用 ->strategy() 方法创建它。

以下是一个RESTful策略的示例

use Lead\Router\Router;
use My\Custom\Namespace\ResourceStrategy;

Router::strategy('resource', new ResourceStrategy());

$router = new Router();
$router->resource('Home', ['namespace' => 'App\Resource']);

// Now all the following URL can be routed
$router->route('home');
$router->route('home/123');
$router->route('home/add');
$router->route('home', 'POST');
$router->route('home/123/edit');
$router->route('home/123', 'PATCH');
$router->route('home/123', 'DELETE');

策略

namespace use My\Custom\Namespace;

class ResourceStrategy {

    public function __invoke($router, $resource, $options = [])
    {
        $path = strtolower(strtr(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $resource), '-', '_'));

        $router->get($path, $options, function($route) {
            return $this->_dispatch($route, $resource, 'index');
        });
        $router->get($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
            return $this->_dispatch($route, $resource, 'show');
        });
        $router->get($path . '/add', $options, function($route) {
            return $this->_dispatch($route, $resource, 'add');
        });
        $router->post($path, $options, function($route) {
            return $this->_dispatch($route, $resource, 'create');
        });
        $router->get($path . '/{id:[0-9a-f]{24}|[0-9]+}' .'/edit', $options, function($route) {
            return $this->_dispatch($route, $resource, 'edit');
        });
        $router->patch($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
            return $this->_dispatch($route, $resource, 'update');
        });
        $router->delete($path . '/{id:[0-9a-f]{24}|[0-9]+}', $options, function($route) {
            return $this->_dispatch($route, $resource, 'delete');
        });
    }

    protected function _dispatch($route, $resource, $action)
    {
        $resource = $route->namespace . $resource . 'Resource';
        $instance = new $resource();
        return $instance($route->params, $route->request, $route->response);
    }

}

致谢