在上一篇《Spiral 详细上手指南之安装与配置》中,我们已经基于官方的 WEB 项目模板创建了自己的本地项目 "myapp" 并且已经配置好了数据库连接和用于开发的进程参数。
通过这整个系列,最终将会开发完成一个简化版的博客 APP. 在这次的文章中,暂时不会涉及数据库操作和领域模型相关的开发,而是聚焦于 Spiral 框架的路由(route)和控制器(controller)部分。
我们首先要为博客文章创建路由和控制器,包含以下的路由:
GET "/posts"
: 文章列表页GET "/posts/<id>"
: 文章详情页POST "/posts"
: 创建文章的 APIPUT "/posts"
: 保存文章修改的 APIDELETE "/posts/<id>"
: 删除文章的 API这些路由都会指向我们创建的 PostController
控制器中的对应方法。
前文提到过,由 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 App\Bootloader;
use App\Controller\HomeController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouteInterface;
use Spiral\Router\RouterInterface;
use Spiral\Router\Target\Controller;
use Spiral\Router\Target\Namespaced;
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))
);
Spiral 的路由规则是根据 PSR-15 规范来实现的,在任何一个引导程序中,我们都可以通过依赖 RouterInterface
这个接口,并借助它来注册新的路由规则。这个接口提供了以下方法:
setRoute(string $name, Spiral\Router\RouteInterface $route): void
: 定义路由规则setDefault(Spiral\Router\RouteInterface $route): void
: 定义默认路由规则getRoute(string $name): Spiral\Router\RouteInterface
: 通过名称取回路由规则实例getRoutes(): array
: 取回所有已注册的路由规则集合uri(string $name, array $parameters = []): Psr\Http\message\UriInterface
: 生成 uri可以看到其中setRoute
方法接受两个参数,第一个是字符串,指定路由的名称,第二个是 Spiral\Router\RouteInterface
接口的具体实现,在 Spiral 中 Spiral\Router\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 规范,因此这个参数可以是任何一个实现 Psr\Http\Server\RequestHandlerInterface
接口的对象。比如直接用闭包函数来实现:
new Route(
'/<name>',
function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write("响应内容");
return $response;
}
)
但在实际项目中可能用得更多的是以下几种:
Spiral\Router\Target\Group
: 控制器组(通常在 Restful API 中使用比较多)Spiral\Router\Target\Controller
: 控制器(之前被删掉的自带路由就是这种)Spiral\Router\Target\Action
: 控制器方法(我们前面添加的所有规则都是这种)Spiral\Router\Target\Namespaced
: 命名空间(系统自带的默认规则属于这种)稍后会对这几种不同的路由目标分别介绍。在构造函数之外,Route
类还有几个比较常用的实例方法:
withDefaults(array $defaults): RouteInterface
: 给路由设定参数默认值withVerbs(string ...$verbs): RouteInterface
: 指定路由可用的 HTTP 动词withMiddleware(...$middleware)
: 给路由绑定中间件所以如果需要让某个路由只用于特定的 HTTP 方法(动词),可以在创建了路由实例之后,用 withVerbs
方法实现:
$route = new Route('/foo', new Controller('App\Controller\FooController'));
$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
是可选参数如果要把一条路由规则指向具体的控制器,就可以用到上面提到的 Spiral\Router\Target\Controller
这个 target,例如:
use Spiral\Router\Target\Controller;
$route = new Route(
'/posts[/<action>[/<id:\d+>]', // 匹配模式
new Controller(
'App\Controller\PostController', // 目标控制器
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
的时候,一共传入了四个参数,后两个稍后再介绍。
如果希望把路由明确地指向具体的控制器方法而不是整个控制器,那么可以使用 Spiral\Router\Target\Action
这个目标:
use Spiral\Router\Target\Action;
// 匹配 "/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 是 Spiral\Router\Target\Group
:
use Spiral\Router\Target\Group;
// 匹配 "/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 Spiral\Router\Target\Namespaced;
// 匹配 "/foo/bar",指向 "App\Controller\FooController::bar()"
$route = new Route(
'/<controller>[/<action>]',
new Namespaced(
'App\Controller', // 目标命名空间
'Controller', // 控制器类的类名后缀(可选参数,默认值 "Controller")
0, // 是否 Restful 风格(可选参数,默认值 0)
'index' // 默认 action(可选参数,默认值 "index")
)
);
// 匹配 "/admin/foo/bar",指向 "App\Controller\Admin\FooController::bar()"
$route = new Route(
'/admin/<controller>[/<action>]',
new Namespaced(
'App\Controller\Admin', // 目标命名空间
'Controller', // 控制器类名后缀(可选参数,默认值 "Controller")
0, // 是否 Restful 风格(可选参数,默认值 0)
'index' // 默认 action(可选参数,默认值 "index")
)
);
可以看到,我们可以借助这个工具,给前端、后端的路由各设置不同的默认值。
前面一直有提到一个 "是否 Restful 风格" 的参数,这个参数主要为了方便实现 Restful 风格的路由(把相同路径的不同动词请求分开)。如果在创建路由实例的时候指定这个参数为 1
,那么 Spiral 会在解析控制器方法的时候自动把 HTTP 动词加到方法名称前。比如要请求的控制器方法是 foo
,那么 POST 请求会指向 postFoo
,GET 请求会指向 getFoo
.
为了演示这种用法,首先创建一个控制器:
namespace App\Controller;
class FooController
{
public function getBar(int id) {}
public function postBar(int id) {}
public function putBar(int id) {}
public function deleteBar(int id) {}
}
然后定义一个路由规则:
use App\Controller\FooController;
use Spiral\Router\Target\Controller;
$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: App\Controller\PostController.php
*/
declare(strict_types=1);
namespace App\Controller;
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 App\Bootloader;
use App\Controller\HomeController;
+use App\Controller\PostController;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Router\Route;
use Spiral\Router\RouteInterface;
@@ -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+>] | Controller\PostController->* |
| * | /<action>.html | Controller\HomeController->* |
| * | /[<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 框架中的请求和响应。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。