Aop 设计 - 使用 PHP-parser 重写 PHP 类
最近一直在研究 Swoft 框架,框架核心当然是 Aop 切面编程,所以想把这部分的心得记下来,以供后期查阅。
Swoft 新版的 Aop 设计建立在 PHP Parser 上面。所以这片文章,主要介绍一下 PHP Parser 在 Aop 编程中的使用。
简单的来讲,我们想在某些类的方法上进行埋点,比如下面的 Test 类。
class Test {
public function get() {
// do something
}
}
我们想让它的 get 方法变成以下的样子
class Test {
public function get() {
// do something before
// do something
// do something after
}
}
最简单的设计就是,我们使用 parser 生成对应的语法树,然后主动修改方法体内的逻辑。
接下来,我们就是用 PHP Parser 来搞定这件事。
首先我们先定一个 ProxyVisitor。Visitor 有四个方法,其中
- beforeTraverse () 方法用于遍历之前,通常用来在遍历前对值进行重置。
- afterTraverse () 方法和(1)相同,唯一不同的地方是遍历之后才触发。
- enterNode () 和 leaveNode () 方法在对每个节点访问时触发。
<?php
namespace App\Aop;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node;
class ProxyVisitor extends NodeVisitorAbstract
{
public function leaveNode(Node $node)
{
}
public function afterTraverse(array $nodes)
{
}
}
我们要做的就是重写 leaveNode,让我们遍历语法树的时候,把类方法里的逻辑重置掉。另外就是重写 afterTraverse 方法,让我们遍历结束之后,把我们的 AopTrait 扔到类里。AopTrait 就是我们赋予给类的,切面编程的能力。
首先,我们先创建一个测试类,来看看 parser 生成的语法树是什么样子的
namespace App;
class Test
{
public function show()
{
return 'hello world';
}
}
use PhpParser\ParserFactory;
use PhpParser\NodeDumper;
$file = APP_PATH . '/Test.php';
$code = file_get_contents($file);
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
$dumper = new NodeDumper();
echo $dumper->dump($ast) . "\n";
结果树如下
array(
0: Stmt_Namespace(
name: Name(
parts: array(
0: App
)
)
stmts: array(
0: Stmt_Class(
flags: 0
name: Identifier(
name: Test
)
extends: null
implements: array(
)
stmts: array(
0: Stmt_ClassMethod(
flags: MODIFIER_PUBLIC (1)
byRef: false
name: Identifier(
name: show
)
params: array(
)
returnType: null
stmts: array(
0: Stmt_Return(
expr: Scalar_String(
value: hello world
)
)
)
)
)
)
)
)
)
语法树的具体含义,我就不赘述了,感兴趣的同学直接去看一下 PHP Parser 的文档吧。(其实我也没全都看完。。。大体知道而已,哈哈哈)
接下来重写我们的 ProxyVisitor
<?php
namespace App\Aop;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\TraitUse;
use PhpParser\NodeFinder;
class ProxyVisitor extends NodeVisitorAbstract
{
protected $className;
protected $proxyId;
public function __construct($className, $proxyId)
{
$this->className = $className;
$this->proxyId = $proxyId;
}
public function getProxyClassName(): string
{
return \basename(str_replace('\\', '/', $this->className)) . '_' . $this->proxyId;
}
public function getClassName()
{
return '\\' . $this->className . '_' . $this->proxyId;
}
/**
* @return \PhpParser\Node\Stmt\TraitUse
*/
private function getAopTraitUseNode(): TraitUse
{
// Use AopTrait trait use node
return new TraitUse([new Name('\App\Aop\AopTrait')]);
}
public function leaveNode(Node $node)
{
// Proxy Class
if ($node instanceof Class_) {
// Create proxy class base on parent class
return new Class_($this->getProxyClassName(), [
'flags' => $node->flags,
'stmts' => $node->stmts,
'extends' => new Name('\\' . $this->className),
]);
}
// Rewrite public and protected methods, without static methods
if ($node instanceof ClassMethod && !$node->isStatic() && ($node->isPublic() || $node->isProtected())) {
$methodName = $node->name->toString();
// Rebuild closure uses, only variable
$uses = [];
foreach ($node->params as $key => $param) {
if ($param instanceof Param) {
$uses[$key] = new Param($param->var, null, null, true);
}
}
$params = [
// Add method to an closure
new Closure([
'static' => $node->isStatic(),
'uses' => $uses,
'stmts' => $node->stmts,
]),
new String_($methodName),
new FuncCall(new Name('func_get_args')),
];
$stmts = [
new Return_(new MethodCall(new Variable('this'), '__proxyCall', $params))
];
$returnType = $node->getReturnType();
if ($returnType instanceof Name && $returnType->toString() === 'self') {
$returnType = new Name('\\' . $this->className);
}
return new ClassMethod($methodName, [
'flags' => $node->flags,
'byRef' => $node->byRef,
'params' => $node->params,
'returnType' => $returnType,
'stmts' => $stmts,
]);
}
}
public function afterTraverse(array $nodes)
{
$addEnhancementMethods = true;
$nodeFinder = new NodeFinder();
$nodeFinder->find($nodes, function (Node $node) use (
&$addEnhancementMethods
) {
if ($node instanceof TraitUse) {
foreach ($node->traits as $trait) {
// Did AopTrait trait use ?
if ($trait instanceof Name && $trait->toString() === '\App\Aop\AopTrait') {
$addEnhancementMethods = false;
break;
}
}
}
});
// Find Class Node and then Add Aop Enhancement Methods nodes and getOriginalClassName() method
$classNode = $nodeFinder->findFirstInstanceOf($nodes, Class_::class);
$addEnhancementMethods && array_unshift($classNode->stmts, $this->getAopTraitUseNode());
return $nodes;
}
}
trait AopTrait
{
/**
* AOP proxy call method
*
* @param \Closure $closure
* @param string $method
* @param array $params
* @return mixed|null
* @throws \Throwable
*/
public function __proxyCall(\Closure $closure, string $method, array $params)
{
return $closure(...$params);
}
}
当我们拿到节点是类时,我们重置这个类,让新建的类继承这个类。
当我们拿到的节点是类方法时,我们使用 proxyCall 来重写方法。
当遍历完成之后,给类加上我们定义好的 AopTrait。
接下来,让我们执行以下第二个 DEMO
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use App\Aop\ProxyVisitor;
use PhpParser\PrettyPrinter\Standard;
$file = APP_PATH . '/Test.php';
$code = file_get_contents($file);
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
$traverser = new NodeTraverser();
$className = 'App\\Test';
$proxyId = uniqid();
$visitor = new ProxyVisitor($className, $proxyId);
$traverser->addVisitor($visitor);
$proxyAst = $traverser->traverse($ast);
if (!$proxyAst) {
throw new \Exception(sprintf('Class %s AST optimize failure', $className));
}
$printer = new Standard();
$proxyCode = $printer->prettyPrint($proxyAst);
echo $proxyCode;
结果如下
namespace App;
class Test_5b495d7565933 extends \App\Test
{
use \App\Aop\AopTrait;
public function show()
{
return $this->__proxyCall(function () {
return 'hello world';
}, 'show', func_get_args());
}
}
这样就很有趣了,我们可以赋予新建的类一个新的方法,比如 getOriginClassName。然后我们在 proxyCall 中,就可以根据 getOriginClassName 和 $method 拿到方法的精确 ID,在这基础之上,我们可以做很多东西,比如实现一个方法缓存。
我这里呢,只给出一个最简单的示例,就是当返回值为 string 的时候,加上个叹号。
修改一下我们的代码
namespace App\Aop;
trait AopTrait
{
/**
* AOP proxy call method
*
* @param \Closure $closure
* @param string $method
* @param array $params
* @return mixed|null
* @throws \Throwable
*/
public function __proxyCall(\Closure $closure, string $method, array $params)
{
$res = $closure(...$params);
if (is_string($res)) {
$res .= '!';
}
return $res;
}
}
以及在我们的调用代码后面加上以下代码
eval($proxyCode);
$class = $visitor->getClassName();
$bean = new $class();
echo $bean->show();
结果当然和我们预想的那样,打印出了
hello world!
以上设计来自 Swoft 开发组 swoft-component,我只是个懒惰的搬运工,有兴趣的可以去看一下。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: