PHP parser重写PHP类使用示例详解
作者:李铭昕
引言
最近一直在研究 Swoft 框架,框架核心当然是 Aop 切面编程,所以想把这部分的心得记下来,以供后期查阅。
Swoft 新版的 Aop 设计建立在 PHP Parser 上面。所以这片文章,主要介绍一下 PHP Parser 在 Aop 编程中的使用。
Test 类
简单的来讲,我们想在某些类的方法上进行埋点,比如下面的 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,我只是个懒惰的搬运工,有兴趣的可以去看一下。
以上就是PHP parser重写PHP类使用示例详解的详细内容,更多关于PHP parser重写PHP类的资料请关注脚本之家其它相关文章!