注解路由
Testing Is Documentation
QueryPHP 除了传统的自动匹配 MVC 路由之外,也支持自定义的注解路由。
Uses
php
<?php
use Leevel\Di\Container;
use Leevel\Di\IContainer;
use Leevel\Http\Request;
use Leevel\Kernel\App;
use Leevel\Kernel\Utils\Api;
use Leevel\Router\Router;
use Leevel\Router\RouterNotFoundException;
use Leevel\Router\RouterProvider;
use Leevel\Router\ScanRouter;
use Symfony\Component\HttpFoundation\Response;
use Tests\Router\Middlewares\Demo1;
use Tests\Router\Middlewares\Demo2;
use Tests\Router\Middlewares\Demo3;
use Tests\Router\Middlewares\DemoForAll;
use Tests\Router\Middlewares\DemoForBasePath;
注解路由扫描
QueryPHP 系统会根据路由服务提供者信息,扫描系统的注解生成框架的注解路由,并且支持缓存到文件。
fixture 定义
路由服务提供者 Tests\Router\RouterProviderAnnotation
php
namespace Tests\Router;
class RouterProviderAnnotation extends RouterProvider
{
protected ?string $controllerDir = 'Router\\Controllers';
protected array $middlewareGroups = [
'group1' => [
'demo1',
'demo2',
],
'group2' => [
'demo1',
'demo3:10,world',
],
'group3' => [
'demo1',
'demo2',
'demo3:10,world',
],
];
protected array $middlewareAlias = [
'demo1' => Demo1::class,
'demo2' => Demo2::class,
'demo3' => Demo3::class,
'demo_for_base_path' => DemoForBasePath::class,
'demo_for_all' => DemoForAll::class,
];
protected array $basePaths = [
'*' => [
'middlewares' => 'demo_for_all',
],
'/basePath/normalize' => [
'middlewares' => 'demo_for_base_path',
],
];
protected array $groups = [
'pet' => [],
'store' => [],
'user' => [],
'/api/v1' => [
'middlewares' => 'group1',
],
'/api/v2' => [
'middlewares' => 'group2',
],
'/api/v3' => [
'middlewares' => 'demo1,demo3:30,world',
],
'/api/v3' => [
'middlewares' => ['demo1', 'group3'],
],
'/api/v4' => [
'middlewares' => 'notFound',
],
'newPrefix/v1' => [],
];
public function bootstrap(): void
{
parent::bootstrap();
}
public function getRouters(): array
{
return parent::getRouters();
}
protected function makeScanRouter(): ScanRouter
{
$scanRouter = parent::makeScanRouter();
$scanRouter->setControllerDir('');
return $scanRouter;
}
}
路由注解缓存结果 tests/Router/Apps/AppForAnnotation/data.json
json
{
"base_paths": {
"*": {
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\DemoForAll@handle"
],
"terminate": [
"Tests\\Router\\Middlewares\\DemoForAll@terminate"
]
}
},
"\/^\\\/basePath\\\/normalize\\\/$\/": {
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\DemoForBasePath@handle"
]
}
},
"\/^\\\/api\\\/v1(\\S*)\\\/$\/": {
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo2@handle"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate",
"Tests\\Router\\Middlewares\\Demo2@terminate"
]
}
},
"\/^\\\/api\\\/v2(\\S*)\\\/$\/": {
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo3@handle:10,world"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate"
]
}
},
"\/^\\\/api\\\/v3(\\S*)\\\/$\/": {
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo2@handle",
"Tests\\Router\\Middlewares\\Demo3@handle:10,world"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate",
"Tests\\Router\\Middlewares\\Demo2@terminate"
]
}
}
},
"groups": [
"\/pet",
"\/store",
"\/user",
"\/api\/v1",
"\/api\/v2",
"\/api\/v3",
"\/api\/v4",
"\/newPrefix\/v1"
],
"routers": {
"get": {
"static": {
"\/basePath\/normalize\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\BasePath@normalize"
},
"\/bindNotFound\/test\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\BindNotFound@notFound"
},
"\/bindNotFound\/test2\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\BindNotFound"
},
"\/bindNotFound\/test3\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\BindMethodNotFound"
},
"\/bindNotSet\/test\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\BindNotSet@routePlaceholderFoo"
},
"\/bindNotSet\/test2\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\BindNotSet@routePlaceholderBar"
},
"\/extendVar\/test\/": {
"attributes": {
"args1": "hello",
"args2": "world"
},
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\ExtendVar@withExtendVar"
},
"\/middleware\/test\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\Middleware@foo",
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo2@handle"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate",
"Tests\\Router\\Middlewares\\Demo2@terminate"
]
}
},
"\/middleware\/test2\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\Middleware@bar",
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo2@handle",
"Tests\\Router\\Middlewares\\Demo3@handle:10,world"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate",
"Tests\\Router\\Middlewares\\Demo2@terminate"
]
}
},
"\/middleware\/test3\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\Middleware@hello",
"middlewares": {
"handle": [
"Tests\\Router\\Middlewares\\Demo2@handle",
"Tests\\Router\\Middlewares\\Demo3@handle:10,world",
"Tests\\Router\\Middlewares\\DemoForBasePath@handle"
],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate",
"Tests\\Router\\Middlewares\\Demo2@terminate"
]
}
},
"\/middleware\/test4\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\Middleware@world",
"middlewares": {
"handle": [],
"terminate": [
"Tests\\Router\\Middlewares\\Demo1@terminate"
]
}
}
},
"a": {
"_": {
"\/api\/notInGroup\/petLeevel\/{petId:[A-Za-z]+}\/": {
"bind": "\\Tests\\Router\\Apps\\AppForAnnotation\\Controllers\\Pet@petLeevelNotInGroup",
"var": [
"petId"
]
},
"regex": [
"~^(?|\/api\/notInGroup\/petLeevel\/([A-Za-z]+)\/)$~x"
],
"map": [
{
"2": "\/api\/notInGroup\/petLeevel\/{petId:[A-Za-z]+}\/"
}
]
},
"\/api\/v1": {
"\/api\/v1\/petLeevel\/{petId:[A-Za-z]+}\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\PetLeevel",
"var": [
"petId"
]
},
"regex": [
"~^(?|\/api\/v1\/petLeevel\/([A-Za-z]+)\/)$~x"
],
"map": [
{
"2": "\/api\/v1\/petLeevel\/{petId:[A-Za-z]+}\/"
}
]
}
},
"n": {
"\/newPrefix\/v1": {
"\/newPrefix\/v1\/petLeevel\/{petId:[A-Za-z]+}\/": {
"bind": "\\Tests\\Router\\Controllers\\Annotation\\NewPrefix",
"var": [
"petId"
]
},
"regex": [
"~^(?|\/newPrefix\/v1\/petLeevel\/([A-Za-z]+)\/)$~x"
],
"map": [
{
"2": "\/newPrefix\/v1\/petLeevel\/{petId:[A-Za-z]+}\/"
}
]
}
}
}
}
}
php
public function testBaseRouterData(): void
{
$container = $this->createContainer();
$container->singleton('router', $router = $this->createRouter($container));
$provider = new RouterProviderAnnotation($container);
self::assertNull($provider->register());
self::assertNull($provider->bootstrap());
$data = file_get_contents(__DIR__.'/Apps/AppForAnnotation/data.json');
self::assertEquals(
$data,
$this->varJson(
[
'base_paths' => $router->getBasePaths(),
'groups' => $router->getGroups(),
'routers' => $router->getRouters(),
]
)
);
}
基本使用
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Pet::petLeevel
private function petLeevel(): void {};
控制器
php
namespace Tests\Router\Controllers\Annotation;
use Leevel\Di\IContainer;
class PetLeevel
{
public function handle(IContainer $container): string
{
return 'hello plus for petLeevel, attributes petId is '.
$container
->make('request')
->attributes
->get('petId')
;
}
}
php
public function testMatchedPetLeevel(): void
{
$pathInfo = '/api/v1/petLeevel/hello';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = $this->createRequest($pathInfo, $attributes, $method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance('request', $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$router->setControllerDir($controllerDir);
$provider->register();
$provider->bootstrap();
if (isset($GLOBALS['demo_middlewares'])) {
unset($GLOBALS['demo_middlewares']);
}
$response = $router->dispatch($request);
$this->assertInstanceof(Response::class, $response);
self::assertSame('hello plus for petLeevel, attributes petId is hello', $response->getContent());
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares']
)
);
$router->throughTerminateMiddleware($request, $response);
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle",
"DemoForAll::terminate",
"Demo1::terminate",
"Demo2::terminate"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares'],
1
)
);
unset($GLOBALS['demo_middlewares']);
}
基础路径匹配
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\BasePath::foo
private function foo(): void {};
控制器
php
namespace Tests\Router\Controllers\Annotation;
class BasePath
{
public function normalize(): string
{
return 'hello plus for basePath normalize';
}
}
php
public function testMatchedBasePathNormalize(): void
{
$pathInfo = '/basePath/normalize';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = $this->createRequest($pathInfo, $attributes, $method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance('request', $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$provider->register();
$provider->bootstrap();
if (isset($GLOBALS['demo_middlewares'])) {
unset($GLOBALS['demo_middlewares']);
}
$response = $router->dispatch($request);
$this->assertInstanceof(Response::class, $response);
self::assertSame('hello plus for basePath normalize', $response->getContent());
$data = <<<'eot'
[
"DemoForAll::handle",
"DemoForBasePath::handle"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares']
)
);
$router->throughTerminateMiddleware($request, $response);
$data = <<<'eot'
[
"DemoForAll::handle",
"DemoForBasePath::handle",
"DemoForAll::terminate"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares'],
1
)
);
unset($GLOBALS['demo_middlewares']);
}
Attributes 扩展变量匹配
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\ExtendVar::withExtendVar
public function withExtendVar(Request $request): string;
控制器
php
# Tests\Router\Apps\AppForAnnotation\Controllers\ExtendVar::withExtendVar
public function withExtendVar(Request $request): string
{
return 'withExtendVar and attributes are '.
json_encode($request->attributes->all());
}
php
public function testMatchedWithExtendVar(): void
{
$pathInfo = '/extendVar/test';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = new Request([], [], $attributes);
$request->setPathInfo($pathInfo);
$request->setMethod($method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance(Request::class, $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$router->setControllerDir($controllerDir);
$provider->register();
$provider->bootstrap();
$result = $router->dispatch($request);
self::assertSame('withExtendVar and attributes are {"args1":"hello","args2":"world"}', $result->getContent());
}
Middlewares 中间件匹配
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::foo
public function foo(): string;
控制器
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::foo
public function foo(): string
{
return 'Middleware matched';
}
php
public function testMiddleware(): void
{
$pathInfo = '/middleware/test';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = new Request([], [], $attributes);
$request->setPathInfo($pathInfo);
$request->setMethod($method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance(Request::class, $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$router->setControllerDir($controllerDir);
$provider->register();
$provider->bootstrap();
if (isset($GLOBALS['demo_middlewares'])) {
unset($GLOBALS['demo_middlewares']);
}
$result = $router->dispatch($request);
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares']
)
);
$router->throughTerminateMiddleware($request, $result);
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle",
"DemoForAll::terminate",
"Demo1::terminate",
"Demo2::terminate"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares'],
1
)
);
self::assertSame('Middleware matched', $result->getContent());
unset($GLOBALS['demo_middlewares']);
}
Middlewares 中间件匹配支持数组
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::bar
public function bar(): string;
控制器
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::bar
public function bar(): string
{
return 'Middleware matched 2';
}
php
public function testMiddleware2(): void
{
$pathInfo = '/middleware/test2';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = new Request([], [], $attributes);
$request->setPathInfo($pathInfo);
$request->setMethod($method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance(Request::class, $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$router->setControllerDir($controllerDir);
$provider->register();
$provider->bootstrap();
if (isset($GLOBALS['demo_middlewares'])) {
unset($GLOBALS['demo_middlewares']);
}
$result = $router->dispatch($request);
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle",
"Demo3::handle(arg1:10,arg2:world)"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares']
)
);
$router->throughTerminateMiddleware($request, $result);
$data = <<<'eot'
[
"DemoForAll::handle",
"Demo2::handle",
"Demo3::handle(arg1:10,arg2:world)",
"DemoForAll::terminate",
"Demo1::terminate",
"Demo2::terminate"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares'],
1
)
);
self::assertSame('Middleware matched 2', $result->getContent());
unset($GLOBALS['demo_middlewares']);
}
Middlewares 中间件匹配支持类名
fixture 定义
路由定义
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::world
public function world(): string;
控制器
php
# Tests\Router\Apps\AppForAnnotation\Controllers\Middleware::world
public function world(): string
{
return 'Middleware matched 4';
}
php
public function testMiddleware4(): void
{
$pathInfo = '/middleware/test4';
$attributes = [];
$method = 'GET';
$controllerDir = 'Controllers';
$container = $this->createContainer();
$request = new Request([], [], $attributes);
$request->setPathInfo($pathInfo);
$request->setMethod($method);
$container->singleton('router', $router = $this->createRouter($container));
$container->instance(Request::class, $request);
$container->instance(IContainer::class, $container);
$provider = new RouterProviderAnnotation($container);
$router->setControllerDir($controllerDir);
$provider->register();
$provider->bootstrap();
if (isset($GLOBALS['demo_middlewares'])) {
unset($GLOBALS['demo_middlewares']);
}
$result = $router->dispatch($request);
$data = <<<'eot'
[
"DemoForAll::handle"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares']
)
);
$router->throughTerminateMiddleware($request, $result);
$data = <<<'eot'
[
"DemoForAll::handle",
"DemoForAll::terminate",
"Demo1::terminate"
]
eot;
self::assertSame(
$data,
$this->varJson(
$GLOBALS['demo_middlewares'],
1
)
);
self::assertSame('Middleware matched 4', $result->getContent());
unset($GLOBALS['demo_middlewares']);
}