crysalead / router
HTTP 请求路由器
Requires
- php: >=7
- crysalead/collection: ~3.0
- crysalead/net: dev-master
- psr/http-message: ~1.0
Requires (Dev)
- kahlan/kahlan: ~4.6
README
- 兼容 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); } }