Spiral 详细上手指南之路由规则
- 2020 年 3 月 4 日
- 筆記
在上一篇《Spiral 详细上手指南之安装与配置》中,我们已经基于官方的 WEB 项目模板创建了自己的本地项目 "myapp" 并且已经配置好了数据库连接和用于开发的进程参数。
通过这整个系列,最终将会开发完成一个简化版的博客 APP. 在这次的文章中,暂时不会涉及数据库操作和领域模型相关的开发,而是聚焦于 Spiral 框架的路由(route)和控制器(controller)部分。
实践目标
我们首先要为博客文章创建路由和控制器,包含以下的路由:
GET "/posts"
: 文章列表页GET "/posts/<id>"
: 文章详情页POST "/posts"
: 创建文章的 APIPUT "/posts"
: 保存文章修改的 APIDELETE "/posts/<id>"
: 删除文章的 API
这些路由都会指向我们创建的 PostController
控制器中的对应方法。
Spiral 路由绑定介绍
前文提到过,由 Spiral 的 WEB 项目模板创建的项目中,系统已经定义了两组路由规则:
/<action>.html
默认指向HomeController
下对应的方法/<controller>/<action>
指向对应的控制器和方法
两组路由都有默认值,controller
的默认值是 "HomeController",action
的默认值是 "index", 以上一节列出来要创建的路由为例,如果我们想另外定义路由,那么基于系统的默认路由,我们的路径会这样解析:
/blogs
: 调用BlogsController
的index
方法(包括GET
、POST
、PUT
、PATCH
、DELETE
等所有动词都统一映射到这里)/blogs/123
: 无匹配
Spiral 的路由是不可变的,注册之后禁止修改,所以应该在引导程序中进行注册。我们项目下已经有一个专门负责注册路由的引导程序 RoutesBootloader
,打开项目下的 app/src/Bootloader/RoutesBootloader.php
文件,就能看到系统默认注册的路由:
namespace AppBootloader; use AppControllerHomeController; use SpiralBootBootloaderBootloader; use SpiralRouterRoute; use SpiralRouterRouteInterface; use SpiralRouterRouterInterface; use SpiralRouterTargetController; use SpiralRouterTargetNamespaced; class RoutesBootloader extends Bootloader { /** * @param RouterInterface $router */ public function boot(RouterInterface $router): void { // named route $router->addRoute( 'html', new Route('/<action>.html', new Controller(HomeController::class)) ); // fallback (default) route $router->setDefault($this->defaultRoute()); } /** * Default route points to namespace of controllers. * * @return RouteInterface */ protected function defaultRoute(): RouteInterface { // handle all /controller/action like urls $route = new Route( '/[<controller>[/<action>]]', new Namespaced('App\Controller') ); return $route->withDefaults([ 'controller' => 'home', 'action' => 'index' ]); } }
可以看到通过 RouterInterface
提供的 addRoute
方法来定义路由规则。这里要说明一下,addRoute
这个方法已经弃用,应该使用 setRoute
替代。如果你使用时官方的项目模板还没更新,我们可以自己修改一下:
@@ -27,7 +27,7 @@ class RoutesBootloader extends Bootloader public function boot(RouterInterface $router): void { // named route - $router->addRoute( + $router->setRoute( 'html', new Route('/<action>.html', new Controller(HomeController::class)) );
RouterInterface 接口
Spiral 的路由规则是根据 PSR-15 规范来实现的,在任何一个引导程序中,我们都可以通过依赖 RouterInterface
这个接口,并借助它来注册新的路由规则。这个接口提供了以下方法:
setRoute(string $name, SpiralRouterRouteInterface $route): void
: 定义路由规则setDefault(SpiralRouterRouteInterface $route): void
: 定义默认路由规则getRoute(string $name): SpiralRouterRouteInterface
: 通过名称取回路由规则实例getRoutes(): array
: 取回所有已注册的路由规则集合uri(string $name, array $parameters = []): PsrHttpmessageUriInterface
: 生成 uri
可以看到其中setRoute
方法接受两个参数,第一个是字符串,指定路由的名称,第二个是 SpiralRouterRouteInterface
接口的具体实现,在 Spiral 中 SpiralRouterRoute
类实现了这个接口,并且提供了一些方便使用的方法。
Route 类
RouteInterface
接口用来创建具体的路由规则,实现它的 Route
类的构造函数签名如下:
/** * @param string $pattern 网址路径匹配模式 * @param string|callable|RequestHandlerInterface|TargetInterface $target 可调用的路由目标 * @param array $defaults 匹配模式参数的默认值 */ public function __construct(string $pattern, $target, array $defaults = [])
可以看到,第一个参数是字符串,用来匹配网址,第二个参数是路由目标,我们上面用到的是 TargetInterface
类型,但 Spiral 遵循 PSR-15 规范,因此这个参数可以是任何一个实现 PsrHttpServerRequestHandlerInterface
接口的对象。比如直接用闭包函数来实现:
new Route( '/<name>', function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write("响应内容"); return $response; } )
但在实际项目中可能用得更多的是以下几种:
SpiralRouterTargetGroup
: 控制器组(通常在 Restful API 中使用比较多)SpiralRouterTargetController
: 控制器(之前被删掉的自带路由就是这种)SpiralRouterTargetAction
: 控制器方法(我们前面添加的所有规则都是这种)SpiralRouterTargetNamespaced
: 命名空间(系统自带的默认规则属于这种)
稍后会对这几种不同的路由目标分别介绍。在构造函数之外,Route
类还有几个比较常用的实例方法:
withDefaults(array $defaults): RouteInterface
: 给路由设定参数默认值withVerbs(string ...$verbs): RouteInterface
: 指定路由可用的 HTTP 动词withMiddleware(...$middleware)
: 给路由绑定中间件
所以如果需要让某个路由只用于特定的 HTTP 方法(动词),可以在创建了路由实例之后,用 withVerbs
方法实现:
$route = new Route('/foo', new Controller('AppControllerFooController')); $route = $route->withVerbs('post', 'PUT'); // 动词不区分大小写
路由匹配顺序
Spiral 的路由是按照定义它们的先后顺序依次匹配,一旦匹配到任何一条规则,就不再向下。因此务必把更具体的匹配模式放到前面,否则就会失效,比如有两条匹配路径的顺序如下:
"/<action>"
"/blog"
如果按照这样的顺序定义路由,那么 "/blog" 这个路径就会被第一条 "/<action>"
规则匹配,而第二条规则永远不会被命中。
路由参数
在路径匹配模式字符串中,用[]
来指定可选参数,用<>
来指定参数,参数可以用 :
接正则表达式来接参数的格式,例如:
"/<controller>/<action>"
: 匹配 "/user/add", "/blog/view", "/article/list" 这样的路径,controller 和 action 都是必须的,缺少任何一个不会匹配"/<controller>[/<action>]
: 同上,但是这里 action 是可选参数,通常这种情况下需要为 action 指定默认值,不指定的话系统默认是index
"/[<controller>[/<action>]]"
: 同上,但这里 controller 和 action 都是可选的,请注意两个可选参数是嵌套定义的"/article/<action:list|add|save>"
: 这个匹配 "/article/list", "/article/add", "/article/save",在 ":" 后面可以直接列出允许的值,用 "|" 分隔"/articles/<id:d+>"
: 这个匹配 "/articles/1", "/articles/22" 这样的路径,id
参数限制必须是数字"/posts[/<id:d+>]"
: 这个匹配 "/posts", "/posts/222" 这样的路径,跟上一个的区别在于id
是可选参数
路由指向控制器
如果要把一条路由规则指向具体的控制器,就可以用到上面提到的 SpiralRouterTargetController
这个 target,例如:
use SpiralRouterTargetController; $route = new Route( '/posts[/<action>[/<id:d+>]', // 匹配模式 new Controller( 'AppControllerPostController', // 目标控制器 0, // 是否 Restful 风格(可选参数,默认值:0) "index" // 默认的 action,可选参数(默认值:"index") ) );
这个实例定义了一条路由规则,可以匹配以下路径:
"/posts"
: 会调用PostController::index(int $id = null)
方法,传入参数$id = null
"/posts/list"
: 会调用PostController::list(int $id = null)
方法,传入参数$id = null
"/posts/show/32"
: 会调用PostController::show(int $id = null)
方法,传入参数$id = 32
上面的代码中创建 Controller
的时候,一共传入了四个参数,后两个稍后再介绍。
路由指向控制器方法
如果希望把路由明确地指向具体的控制器方法而不是整个控制器,那么可以使用 SpiralRouterTargetAction
这个目标:
use SpiralRouterTargetAction; // 匹配 "/posts/2019", "/posts/2019/12" $route = new Route( '/posts/<year:d{4}>[/<month:d{2}>]', // 匹配模式 new Action( PostController::class, // 目标控制器 'archive', // 目标方法 0 // 是否 Restful 模式(可选参数,默认值 0) ) ); // 匹配 "/posts/create", "/posts/edit", "/posts/save" $route = new Route( '/posts/<action>', // 匹配模式 new Action( PostController::class, // 目标控制器 ['create', 'edit', 'save'], // action 参数的可用值 0 // 是否 Restful 模式(可选参数,默认值 0) ) );
这里举了两种使用示例,第一种是直接指向明确的某一个控制器方法,第二种是同时制定多个控制器方法。
路由指向控制器组
这个有点像是把多个指向控制器的路由简化成一组的写法,使用的 target 是 SpiralRouterTargetGroup
:
use SpiralRouterTargetGroup; // 匹配 "/home/*", "/demo/*" $route = new Route( '/<controller>/<action>', new Group( [ 'home' => HomeController::class, 'demo' => DemoController::class ], 0, // 是否 Restful 风格(可选参数,默认值 0) 'index' // 默认 action(可选参数,默认值 "index") ) );
所以这个基本上不用做多少解释,基本上就是跟指向控制器的定义一样的,只是可以一次定义多个控制器匹配而已,要说明的是最后一个参数(指定默认 action)是只有把 <action>
指定为可选参数才有意义。
指向命名空间
这个就是系统用来定义默认控制器的方法,通常借助这个,可以实现给自己的项目的路由划分 "module",从而实现 HMVC 结构。例如:
use SpiralRouterTargetNamespaced; // 匹配 "/foo/bar",指向 "AppControllerFooController::bar()" $route = new Route( '/<controller>[/<action>]', new Namespaced( 'AppController', // 目标命名空间 'Controller', // 控制器类的类名后缀(可选参数,默认值 "Controller") 0, // 是否 Restful 风格(可选参数,默认值 0) 'index' // 默认 action(可选参数,默认值 "index") ) ); // 匹配 "/admin/foo/bar",指向 "AppControllerAdminFooController::bar()" $route = new Route( '/admin/<controller>[/<action>]', new Namespaced( 'AppControllerAdmin', // 目标命名空间 'Controller', // 控制器类名后缀(可选参数,默认值 "Controller") 0, // 是否 Restful 风格(可选参数,默认值 0) 'index' // 默认 action(可选参数,默认值 "index") ) );
可以看到,我们可以借助这个工具,给前端、后端的路由各设置不同的默认值。
Restful 风格控制器方法
前面一直有提到一个 "是否 Restful 风格" 的参数,这个参数主要为了方便实现 Restful 风格的路由(把相同路径的不同动词请求分开)。如果在创建路由实例的时候指定这个参数为 1
,那么 Spiral 会在解析控制器方法的时候自动把 HTTP 动词加到方法名称前。比如要请求的控制器方法是 foo
,那么 POST 请求会指向 postFoo
,GET 请求会指向 getFoo
.
为了演示这种用法,首先创建一个控制器:
namespace AppController; class FooController { public function getBar(int id) {} public function postBar(int id) {} public function putBar(int id) {} public function deleteBar(int id) {} }
然后定义一个路由规则:
use AppControllerFooController; use SpiralRouterTargetController; $fooRoute = new Route( '/foo/<id:d+>', new Controller( FooController::class, 1, // 这里改为 1,或者 Controller::RESTFUL 常量 ), ['action' => 'bar'] // 默认值 ); $router->setRoute( 'foo.restful', $fooRoute );
这样当我们以 GET 方法请求 /foo/222
的时候,会执行 getBar
方法,用 DELETE 方法请求 /foo/222
的时候,会请求 deleteBar
方法。
实现我们需要的路由
经过以上这么细致(或者说啰嗦)的介绍之后,回头来看我们要定义的路由,会发现在路径只有两种形式:/posts
和 /posts/<id>
,如果把 id
变成可选参数,那么就只有一种形式:/posts[/<id>]
,而动词有四种:GET
, POST
, PUT
, DELETE
. 很显然,有很多种方案可以实现我们的实践目标。不过个人觉得最简洁的当然是 “路由指向控制器 + Restful 风格”。
创建控制器
首先,创建 PostController
,可以在 app/src/Controller
目录下自己创建这个类,也可以借助脚手架工具,在命令行执行:
$ php app.php create:controller post
控制器的代码如下:
<?php /** * File: AppControllerPostController.php */ declare(strict_types=1); namespace AppController; class PostController { public function getPost(int $id = null): string { return is_int($id) ? "查看文章 $id" : "文章列表"; } public function postPost($id = null): string { return "创建文章"; } public function putPost(int $id = null): string { return "编辑文章"; } public function deletePost(int $id = null): string { return is_int($id) ? "删除文章 $id" : "参数缺失"; } }
定义路由规则
然后打开 app/src/Bootloader/RoutesBootloader.php
,在 boot
方法中注册我们的路由(注意要把我们的规则放到最前面):
--- a/app/src/Bootloader/RoutesBootloader.php +++ b/app/src/Bootloader/RoutesBootloader.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace AppBootloader; use AppControllerHomeController; +use AppControllerPostController; use SpiralBootBootloaderBootloader; use SpiralRouterRoute; use SpiralRouterRouteInterface; @@ -26,8 +27,17 @@ class RoutesBootloader extends Bootloader */ public function boot(RouterInterface $router): void { + $router->setRoute( + 'posts', + new Route( + "/posts[/<id:d+>]", + new Controller(PostController::class, Controller::RESTFUL), + ['action' => 'post'] + ) + ); + // named route - $router->addRoute( + $router->setRoute( 'html', new Route('/<action>.html', new Controller(HomeController::class)) );
重要提醒:如果应用服务器是运行中的,请执行
./spiral http:reset
重设 HTTP 工作进程,或者直接停止再重新运行 spiral 应用服务器。
验证一下
脚手架提供了一个命令可以让我们查看所有已经注册了的路由规则:
$ php app.php route:list +--------+----------------------------+------------------------------+ | Verbs: | Pattern: | Target: | +--------+----------------------------+------------------------------+ | * | /posts[/<id:d+>] | ControllerPostController->* | | * | /<action>.html | ControllerHomeController->* | | * | /[<controller>[/<action>]] | Controller*Controller->* | +--------+----------------------------+------------------------------+
然后我们可以通过 curl
来验证一下:
$ curl http://localhost:8080/posts 文章列表 $ curl http://localhost:8080/posts/2 查看文章 2 $ curl -X POST http://localhost:8080/posts 创建文章 $ curl -X PUT http://localhost:8080/posts 编辑文章 $ curl -X DELETE http://localhost:8080/posts/33 删除文章 33
不足之处
至此,我们本次的实践目标就达到了。当然,严格来说还有一点不足之处,POST
和 PUT
路由严格来说不应该支持 <id>
参数,但现在 [POST|PUT] /posts/333
和 [POST|PUT] /posts
都是一样的。如果要严格限制的话,可以把我们的路由拆成两条,一条包含必备参数 <id>
,一条不含 <id>
参数。或者直接不使用 Restful
风格的路由定义,通过 withVerbs
方法自行绑定路由允许的动词。
如果您有兴趣,可以自行尝试。
在本文中原计划是要把路由和控制器一并介绍给大家,但是写下来发现仅仅是路由的部分就占用了大量的篇幅,而控制器又涉及到了请求和响应两个方面的处理,同样篇幅不短,因此我决定把控制器的部分放到下一篇文章中,详细介绍 Spiral 框架中的请求和响应。