【手把手教你】自定义Swoft 注解和AOP

序言

前两篇关于Swoft RPC 的使用教程得到很多小伙伴的支持

并且反映网上很多关于Swoft 的讲解太过于深♂入, 很多小伙伴看不懂

这是很正常的, 因为开发者们对于使用难度的理解跟我们有些偏差

身为开发者自然知道如何使用, 认为不需要讲解, 也就是所谓的知识的诅咒

所以很多关于Swoft 的讲解都是底层原理

那在这我就开一个大佬们不屑的新手向讲解教程 ( ̄_  ̄ )

叫做[手把手教你]系列

只要跟着一点点练习, 熟练使用是没问题滴

前置技能

  1. 会使用Swoft 的MVC
  2. 使用了Swoft 的注解
  3. 理解注解只是配置的另一种展现方式
  4. PHPStorm 安装了PHP Annotations 插件

下面就让我们开始吧

注解类

想要定义一个注解是非常简单的

新建注解类app\Module\Test\Annotations\Test.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace App\Module\Test\Annotations;

/**
* 测试注解
* @Annotation
* @Target("ALL")
*/
class Test
{
/**
* @var string 构造参数
*/
private $name = '';

public function __construct(array $values)
{
if (isset($values['name'])) {
$this->name = $values['name'];
}
}

/**
* @return string
*/
public function getName(): string
{
return $this->name;
}

/**
* @param string $name
*/
public function setName(string $name)
{
$this->name = $name;
}
}

这里有三点需要注意:

1 类注解要加@Annotation, 用来声明这是一个注解类

2 类名不需要加Annotation后缀

3 类注解@Target()Doctrine\Common\Annotations\Annotation\Target.php, 参数可以填ALL|CLASS|METHOD|PROPERTY|ANNOTATION, 表示该注解使用的级别, 类注解还是方法注解,属性注解,或者全部都能使用

这样我们自定义的注解标签就完成了

IndexController.php里试一下

title

注意不要忘记use我们的注解类

注解类Test@Target设置为ALL, 可以同时在类和方法上使用(属性也可以的)

为了体现使用了注解, 可以在注解类Test的构造函数中进行输出
title

重启一下程序

title

这样就是成功了, 输出多个是正常现象, 毕竟多进程, 也是Swoft高性能的原因

注解解析类 Parser

正如前置技能里所说

注解只是配置的另一种展现方式

任何逻辑都不要在注解类里处理

新建注解解析类app\Module\Test\Parser\TestParser.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Module\Test\Parser;

use App\Module\Test\Collector\TestCollector;
use Swoft\Bean\Parser\AbstractParser;

class TestParser extends AbstractParser
{
public function parser(
string $className,
$objectAnnotation = null,
string $propertyName = '',
string $methodName = '',
$propertyValue = null
)
{
TestCollector::collect($className, $objectAnnotation, $propertyName, $methodName, $propertyValue);
}
}

解释一下这几个参数的意义:

$className 当前注解所在的类名

$objectAnnotation 当前注解所实例化的注解类new Test([name="666"])

$propertyName 当前注解所在的属性名(如果是属性注解)

$methodName 当前注解所在的方法名(如果是方法注解)

$propertyValue 当前注解所在的属性(如果是属性注解)

这里也不要处理逻辑, 因为此刻程序还处于初始化阶段, 没有请求数据

注解解析类Parser只做了一件事, 就是把注解类存入注解收集类

什么是注解收集类呢?

注解收集类 Collector

注解收集类Collector非常简单, 相当于一个全局数组方便我们后续处理而已

新建注解收集类app\Module\Test\Collector\TestCollector.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Module\Test\Collector;

use Swoft\Bean\CollectorInterface;

class TestCollector implements CollectorInterface
{
private static $test = [];

public static function collect(
string $className,
$objectAnnotation = null,
string $propertyName = '',
string $methodName = '',
$propertyValue = null
)
{
self::$test[$className][$methodName] = $objectAnnotation;
}

public static function getCollector()
{
return self::$test;
}
}

正如所见, 只是存取$objectAnnotation注解实例, 方便我们后面使用

本篇举例的是方法注解, 所以默认$methodName不为空

注解收集类Collector被注解解析类Parser调用

那注解解析类Parser被谁调用呢?

注解封装类 Wrapper

新建注解封装类app\Module\Test\Wrapper\TestWrapper.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

namespace App\Module\Test\Wrapper;

use App\Module\Test\Annotations\Test;
use Swoft\Bean\Wrapper\AbstractWrapper;

class TestWrapper extends AbstractWrapper
{
/**
* @var array 解析哪些注解(类级)
*/
protected $classAnnotations = [];

/**
* @var array 解析哪些注解(属性级)
*/
protected $propertyAnnotations = [];

/**
* @var array 解析哪些注解(方法级)
*/
protected $methodAnnotations = [
Test::class,
];

/**
* 是否解析类注解
* @param array $annotations
* @return bool
*/
public function isParseClassAnnotations(array $annotations): bool
{
return false;
}

/**
* 是否解析属性注解
* @param array $annotations
* @return bool
*/
public function isParsePropertyAnnotations(array $annotations): bool
{
return false;
}

/**
* 是否解析方法注解
* @param array $annotations
* @return bool
*/
public function isParseMethodAnnotations(array $annotations): bool
{
return true;
}
}

当我们的类注解被实例化时, 会触发注解封装类{注解标签名}Wrapper

注解封装类Wrapper来决定是否触发注解解析类Parser

解释一下, 可能不好理解

这里isParseClassAnnotations 返回false, 意味着类注解略过不解析;

同理isParsePropertyAnnotations 返回false, 属性注解略过不解析;

isParseMethodAnnotations 返回true,
那么对应的$methodAnnotations里的注解类全部触发解析,
这里的Test::class会触发对应的TestParser;

抽象的说, 注解封装类Wrapper回答了两个问题: 是否解析, 解析哪些

具象的说, 这里的封装类TestWrapper回答的就是:”只解析方法注解@Test

可以在注解解析类Parser的构造方法中进行输出
title

重启程序, 我们来看一下!
title
这里可以看到, 在注解类Test里输出的内容还有类注解的666, 到了注解解析类TestParser, 就没有了666, 只有方法注解777

总结

实例化注解类Test, 询问注解封装类TestWrapper解析哪些注解

注解封装类TestWrapper回答解析方法上的Test, 于是方法级的注解Test被注解解析类TestParser存进了注解收集类TestCollector

注意!!!!

程序只会主动扫描类注解, 然后扫描类注解Wrapper下指定的方法注解和属性注解!!

如果你的方法注解不存在于任何类注解的Wrapper下, 则是不会被解析的!!

如何使用

收集完毕下面举例如何使用

中间件

新建中间件app\Module\Test\Middlewares\TestMiddleware.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace App\Module\Test\Middlewares;

use App\Module\Test\Annotations\Test;
use App\Module\Test\Collector\TestCollector;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swoft\Bean\Annotation\Bean;
use Swoft\Core\RequestContext;
use Swoft\Http\Message\Middleware\MiddlewareInterface;

/**
* @Bean()
*/
class TestMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
echo "in Middleware\n";
$response = $handler->handle($request);

// 当前请求的控制器
$controllerClass = RequestContext::getContextDataByKey('controllerClass');
// 当前请求的方法
$controllerAction = RequestContext::getContextDataByKey('controllerAction');

$collector = TestCollector::getCollector();

/* @var Test $test*/
if ($test = $collector[$controllerClass][$controllerAction]){
print_r($test->getName()." in Middleware\n");
}
return $response;
}
}

这里可以看到, 之前负责收集注解的注解收集类TestCollector 派上用处了, 可以非常方便的通过他获取到当前请求方法的注解信息

这需要注意, RequestContext::getContextDataByKey('controllerClass')获取请求控制器的方法要在$handler->handle($request)后才有值, 因为负责路由的中间件还没执行到XD

title
切勿忘记use 中间件

重启完访问一下试试
title

AOP 切面

新建切面类app\Module\Test\Aspect\TestAspect.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace App\Module\Test\Aspect;

use App\Module\Test\Collector\TestCollector;
use Swoft\Aop\ProceedingJoinPoint;
use Swoft\Bean\Annotation\Around;
use Swoft\Bean\Annotation\Aspect;
use Swoft\Bean\Annotation\PointAnnotation;
use App\Module\Test\Annotations\Test;
use Swoft\Core\RequestContext;

/**
* 测试切面类
* @Aspect()
* @PointAnnotation(
* include={
* Test::class
* }
* )
* @package App\Module\Test\Aspect
*/
class TestAspect
{
/**
* @Around()
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
*/
public function around(ProceedingJoinPoint $proceedingJoinPoint)
{
$controllerClass = RequestContext::getContextDataByKey('controllerClass');
$controllerAction = RequestContext::getContextDataByKey('controllerAction');

$collector = TestCollector::getCollector();

/* @var Test $test*/
$test = $collector[$controllerClass][$controllerAction];

echo $test->getName(), "around-before\n";
$result = $proceedingJoinPoint->proceed();
echo $test->getName(), "around-after\n";
return $result;
}
}

@Aspect() 声明这是一个切面类

@PointAnnotation 设置了注解切入点

@Before() 设置了通知点

其他设置可以参考Swoft文档 AOP章节;

还需要改一下注解解析类TestParser

parser 方法中加入Collector::$methodAnnotations[$className][$methodName][] = get_class($objectAnnotation);
title

这一步的作用是将使用注解的方法加入到允许切入的数组, 只有在Collector::$methodAnnotations变量中存在的方法才会调用切面

重启程序请求一下
title

控制器

当然也可以在控制器用
title
title

个人还是推荐用切面, 控制器里只需要加入注解即可