对于一个框架来说路由是一件非常重要的事情,可以说是框架的核心之一吧。路由的使用便捷和理解复杂度以及性能对整个框架来说至关重要。
Route::middleware(['first', 'second'])->group(function () {
Route::get('/', function () {
// 使用 first 和 second 中间件
});
Route::get('user/profile', function () {
// 使用 first 和 second 中间件
});
});
Route::group('blog', function () {
Route::rule(':id', 'blog/read');
Route::rule(':name', 'blog/read');
})->ext('html')->pattern(['id' => '\d+', 'name' => '\w+']);
// 匹配 /user/42,不匹配 /user/xyx
$r->addRoute('GET', '/user/{id:\d+}', 'handler');
// 匹配 /user/foobar,不匹配 /user/foo/bar
$r->addRoute('GET', '/user/{name}', 'handler');
// 匹配 /user/foobar,也匹配 /user/foo/bar
$r->addRoute('GET', '/user/{name:.+}', 'handler');
https://www.yiichina.com/doc/guide/2.0/runtime-routing
上面的这些路由都还不错,用起来很方便,我们是否可以改进一下这种东西呢。
我搬了 4 年多砖,工作中只用过一种框架就是 TP3,我还是很喜欢 TP3 这种方式。我也非常喜欢这种自动映射控制器的路由设计,简洁轻松的感觉。也喜欢用 swagger-php 写写文档,搞搞正则路由。
先看看效果,这是 QueryPHP 框架的路由最终效果:
composer create-project hunzhiwange/queryphp myapp dev-master --repository=https://packagist.laravel-china.org/
php leevel server <Visite http://127.0.0.1:9527/>
Home http://127.0.0.1:9527/
Mvc router http://127.0.0.1:9527/api/test
Mvc restful router http://127.0.0.1:9527/restful/123
Mvc restful router with method http://127.0.0.1:9527/restful/123/show
Annotation router http://127.0.0.1:9527/api/v1/petLeevelForApi/helloworld
Annotation router with bind http://127.0.0.1:9527/api/v2/withBind/foobar
很多时候我们不是特别关心它是 GET POST,我们就想简单输入一个地址就可以访问到我们的控制器。
/ = App\App\Controller\Home::show()
/controller/action = App\App\Controller\Controller::action()
/:blog/controller/action = Blog\App\Controller\Controller::action()
/dir1/dir2/dir3/controller/action = App\App\Controller\Dir1\Dir2\Dir3\Controller::action()
如果 action 对应的类存在,则 action 为 class,入口方法为 handle 或则 run,代码实现. https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/Router.php#L598
单元测试用例 https://github.com/hunzhiwange/framework/blob/master/tests/Router/RouterTest.php#L61
上面这种就是一个简单粗暴的路由,简单有效,对很多简单的后台系统非常有效。
restful 已经是一种开发主流,很多路由都在向这一方向靠近,代码实现。 https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/Router.php#L541
单元测试 https://github.com/hunzhiwange/framework/blob/master/tests/Router/RouterTest.php#L205
我们访问同一个 url 的时候,根据不同的请求访问不同的后台
/car/5 GET = App\App\Controller\Car::show()
/car/5 POST = App\App\Controller\Car::store()
/car/5 DELETE = App\App\Controller\Car::destroy()
/car/5 PUT = App\App\Controller\Car::update()
restful 路由自动路由也是 pathInfo 一种,我们系统会分析 pathInfo,会将数字类数据扔进 params,其它字符将会合并进行上面的自动路由解析,一旦发现没有 action 将会通过请求方法自动插入请求的 action.
protected function normalizePathsAndParams(array $data): array
{
$paths = $params = [];
$k = 0;
foreach ($data as $item) {
if (is_numeric($item)) {
$params['_param'.$k] = $item;
$k++;
} else {
$paths[] = $item;
}
}
return [
$paths,
$params,
];
}
贴心转换
/he_llo-wor/Bar/foo/xYY-ac/controller_xx-yy/action-xxx_Yzs => App\App\Controller\HeLloWor\Bar\Foo\XYYAc\ControllerXxYy::actionXxxYzs()
单元测试 https://github.com/hunzhiwange/framework/blob/master/tests/Router/RouterTest.php#L156
上面是一种预热,我们的框架路由设计是这样,优先进行 pathinfo 解析,如果解析失败将进入注解路由高级解析阶段。
预警:注解路由匹配比较复杂,单元测试 100% 覆盖 https://github.com/hunzhiwange/framework/blob/master/tests/Router/RouterAnnotationTest.php
http://127.0.0.1:9527/api = openapi 3
http://127.0.0.1:9527/apis/ = swagger-ui
http://127.0.0.1:9527/api/v1/petLeevelForApi/helloworld = 路由
swagger-ui
swagger-php 3 注释生成的 openapi 3
路由结果
http://127.0.0.1:9527/api/v1/petLeevelForApi/helloworld
Hi you,i am petLeevelForApi and it petId is helloworld
在工作大量使用 swagger-php 来生成注释文档,swagger 有 GET,POST,没错它就是路由,既然如此我们何必在定义一个 router.php 来搞路由。
/**
* @OA\Get(
* path="/api/v1/petLeevelForApi/{petId:[A-Za-z]+}/",
* tags={"pet"},
* summary="Just test the router",
* operationId="petLeevelForApi",
* @OA\Parameter(
* name="petId",
* in="path",
* description="ID of pet to return",
* required=true,
* @OA\Schema(
* type="integer",
* format="int64"
* )
* ),
* @OA\Response(
* response=405,
* description="Invalid input"
* ),
* security={
* {"petstore_auth": {"write:pets", "read:pets"}}
* },
* leevelParams={"args1": "hello", "args2": "world"}
* )
*
* @param mixed $petId
*/
public function petLeevelForApi($petId)
{
return sprintf('Hi you,i am petLeevelForApi and it petId is %s', $petId);
}
VS
Route::get('/', function () {
// 使用 first 和 second 中间件
});
QueryPHP 的注解路由,在标准 swagger-php 的基础上增加了自定义属性扩展功能
leevelScheme="https",
leevelDomain="{subdomain:[A-Za-z]+}-vip.{domain}",
leevelParams={"args1": "hello", "args2": "world"},
leevelMiddlewares="api"
leevelBind="\XXX\XXX\class@method"
leevelBind 未设置自动绑定当前 class 和方法,如果注释写到控制器上,也可以放到空文件等地方,这个时候没有上下文方法和 class,需要绑定 leevelBind leevelBind 可以绑定 class (默认方法 handle or run ),通过 @ 自定义
/api/v1/petLeevelForApi/{petId:[A-Za-z]+}/
基于 leevelGroup 自定义属性支持
* @OA\Tag(
* name="pet",
* leevelGroup="pet",
* description="Everything about your Pets",
* @OA\ExternalDocumentation(
* description="Find out more",
* url="http://swagger.io"
* )
* )
* @OA\Tag(
* name="store",
* leevelGroup="store",
* description="Access to Petstore orders",
* )
* @OA\Tag(
* name="user",
* leevelGroup="user",
* description="Operations about user",
* @OA\ExternalDocumentation(
* description="Find out more about store",
* url="http://swagger.io"
* )
* )
* @OA\ExternalDocumentation(
* description="Find out more about Swagger",
* url="http://swagger.io",
* leevels={
* "*": {
* "middlewares": "common"
* },
* "foo/*world": {
* "middlewares": "custom"
* },
* "api/test": {
* "middlewares": "api"
* },
* "/api/v1": {
* "middlewares": "api",
* "group": true
* },
* "api/v2": {
* "middlewares": "api",
* "group": true
* },
* "/web/v1": {
* "middlewares": "web",
* "group": true
* },
* "web/v2": {
* "middlewares": "web",
* "group": true
* }
* }
* )
使用 php leevel router:cache 生成路由缓存 runtime/bootstrap/router.php
我写一个解析模块来生成路由,这就是我们真正的路由,一个基于标准 swagger-php 的注解路由。
https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/OpenApiRouter.php
<?php /* 2018-09-26 19:00:27 */ ?>
<?php return array (
'base_paths' =>
array (
'*' =>
array (
'middlewares' =>
array (
'handle' =>
array (
),
'terminate' =>
array (
0 => 'Leevel\\Log\\Middleware\\Log@terminate',
),
),
),
'/^\\/foo\\/(\\S+)world\\/$/' =>
array (
'middlewares' =>
array (
),
),
'/^\\/api\\/test\\/$/' =>
array (
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Throttler\\Middleware\\Throttler@handle:60,1',
),
'terminate' =>
array (
),
),
),
),
'group_paths' =>
array (
'/api/v1' =>
array (
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Throttler\\Middleware\\Throttler@handle:60,1',
),
'terminate' =>
array (
),
),
),
'/api/v2' =>
array (
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Throttler\\Middleware\\Throttler@handle:60,1',
),
'terminate' =>
array (
),
),
),
'/web/v1' =>
array (
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Session\\Middleware\\Session@handle',
),
'terminate' =>
array (
0 => 'Leevel\\Session\\Middleware\\Session@terminate',
),
),
),
'/web/v2' =>
array (
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Session\\Middleware\\Session@handle',
),
'terminate' =>
array (
0 => 'Leevel\\Session\\Middleware\\Session@terminate',
),
),
),
),
'groups' =>
array (
0 => '/pet',
1 => '/store',
2 => '/user',
),
'routers' =>
array (
'get' =>
array (
'p' =>
array (
'/pet' =>
array (
'/api/v1/petLeevelForApi/{petId:[A-Za-z]+}/' =>
array (
'params' =>
array (
'args1' => 'hello',
'args2' => 'world',
),
'bind' => '\\App\\App\\Controller\\Petstore\\Api@petLeevelForApi',
'var' =>
array (
0 => 'petId',
),
),
'/api/v2/petLeevel/{petId:[A-Za-z]+}/' =>
array (
'scheme' => 'https',
'domain' => '{subdomain:[A-Za-z]+}-vip.{domain}.queryphp.cn',
'params' =>
array (
'args1' => 'hello',
'args2' => 'world',
),
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Throttler\\Middleware\\Throttler@handle:60,1',
),
'terminate' =>
array (
),
),
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@petLeevel',
'domain_regex' => '/^([A-Za-z]+)\\-vip\\.(\\S+)\\.queryphp\\.cn$/',
'domain_var' =>
array (
0 => 'subdomain',
1 => 'domain',
),
'var' =>
array (
0 => 'petId',
),
),
'/pet/{petId}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@getPetById',
'var' =>
array (
0 => 'petId',
),
),
'/web/v1/petLeevelForWeb/{petId:[A-Za-z]+}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Web@petLeevelForWeb',
'var' =>
array (
0 => 'petId',
),
),
'regex' =>
array (
0 => '~^(?|/api/v1/petLeevelForApi/([A-Za-z]+)/|/api/v2/petLeevel/([A-Za-z]+)/()|/pet/(\\S+)/()()|/web/v1/petLeevelForWeb/([A-Za-z]+)/()()())$~x',
),
'map' =>
array (
0 =>
array (
2 => '/api/v1/petLeevelForApi/{petId:[A-Za-z]+}/',
3 => '/api/v2/petLeevel/{petId:[A-Za-z]+}/',
4 => '/pet/{petId}/',
5 => '/web/v1/petLeevelForWeb/{petId:[A-Za-z]+}/',
),
),
),
),
'static' =>
array (
'/api/v2/petLeevelV2Api/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Api@petLeevelV2ForApi',
),
'/pet/findByTags/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@findByTags',
),
'/store/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Store@getInventory',
),
'/user/login/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@loginUser',
),
'/user/logout/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@logoutUser',
),
'/web/v2/petLeevelV2Web/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Web@petLeevelV2ForWeb',
),
),
'w' =>
array (
'_' =>
array (
'/api/v2/withBind/{petId:[A-Za-z]+}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@withBind',
'middlewares' =>
array (
'handle' =>
array (
0 => 'Leevel\\Throttler\\Middleware\\Throttler@handle:60,1',
),
'terminate' =>
array (
),
),
'var' =>
array (
0 => 'petId',
),
),
'regex' =>
array (
0 => '~^(?|/api/v2/withBind/([A-Za-z]+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/api/v2/withBind/{petId:[A-Za-z]+}/',
),
),
),
),
's' =>
array (
'/store' =>
array (
'/store/order/{orderId}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Store@getOrderById',
'var' =>
array (
0 => 'orderId',
),
),
'regex' =>
array (
0 => '~^(?|/store/order/(\\S+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/store/order/{orderId}/',
),
),
),
),
'u' =>
array (
'/user' =>
array (
'/user/{username}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@getUserByName',
'var' =>
array (
0 => 'username',
),
),
'regex' =>
array (
0 => '~^(?|/user/(\\S+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/user/{username}/',
),
),
),
),
),
'post' =>
array (
'static' =>
array (
'/pet/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@addPet',
),
'/store/order/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Store@placeOrder',
),
'/user/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@createUser',
),
'/user/createWithArray/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@createUsersWithListInput',
),
),
'p' =>
array (
'/pet' =>
array (
'/pet/{petId}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@updatePetWithForm',
'var' =>
array (
0 => 'petId',
),
),
'/pet/{petId}/uploadImage/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@uploadFile',
'var' =>
array (
0 => 'petId',
),
),
'regex' =>
array (
0 => '~^(?|/pet/(\\S+)/|/pet/(\\S+)/uploadImage/())$~x',
),
'map' =>
array (
0 =>
array (
2 => '/pet/{petId}/',
3 => '/pet/{petId}/uploadImage/',
),
),
),
),
),
'delete' =>
array (
'p' =>
array (
'/pet' =>
array (
'/pet/{petId}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Pet@deletePet',
'var' =>
array (
0 => 'petId',
),
),
'regex' =>
array (
0 => '~^(?|/pet/(\\S+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/pet/{petId}/',
),
),
),
),
's' =>
array (
'/store' =>
array (
'/store/order/{orderId}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\Store@deleteOrder',
'var' =>
array (
0 => 'orderId',
),
),
'regex' =>
array (
0 => '~^(?|/store/order/(\\S+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/store/order/{orderId}/',
),
),
),
),
'u' =>
array (
'/user' =>
array (
'/user/{username}/' =>
array (
'bind' => '\\App\\App\\Controller\\Petstore\\User@deleteUser',
'var' =>
array (
0 => 'username',
),
),
'regex' =>
array (
0 => '~^(?|/user/(\\S+)/)$~x',
),
'map' =>
array (
0 =>
array (
2 => '/user/{username}/',
),
),
),
),
),
),
); ?>
有了路由,当然就是路由匹配问题,我们在路由中参考了 composer 第一个字母分组,分组路由分组,以及基于 fastrouter 合并路由正则分组,大幅度提高了路由匹配性能。
https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/Match/Annotation.php
其中 fastRouter 实现代码如下: https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/Match/Annotation.php#L162 https://github.com/hunzhiwange/framework/blob/master/src/Leevel/Router/OpenApiRouter.php#L259
路由文档一步搞定就是这么简单,它和 lavarel 等框架的路由一样强大,没有很多记忆的东西,强制你使用 swagger,写路由写文档。
路由组件 100% 单元测试覆盖。 QueryPHP 路由已经全部完结,目前剩余 55% 数据库层和 auth 目前的单元测试,QueryPHP 全力编写单元测试做为重构,即将上线 alpah 版本。八年磨一剑,只珍朝夕。我们只想为中国 PHP 业界提供 100% 单元测试覆盖的框架竟在 QueryPHP.
官网重新出发 https://www.queryphp.com/
1
doyouhaobaby OP 时间比较仓促,睡觉,明天要搬砖。
|
2
linxl 2018-09-27 09:10:18 +08:00
弱弱的问一下,写注释不会写疯吗
|
3
doyouhaobaby OP @linxl 需求分析完,都是后端先写文档,然后一起讨论是否合理,然后 easymock 基于生成的 swagger json 来生成 mock 数据,前端分开做,最后一起联调。实际上写文档是必须的,复制粘贴改改就行。
|