在laravel下使用php-parser实现aop
composer require nikic/php-parser
Test.php
<?php /** * Created by PhpStorm. * User: CSH * Date: 2019/4/4 * Time: 11:26 */ namespace app; /** * 为该类创建代理,并植入切面 埋点 * 使用parser生成对应的语法树,然后主动修改方法体内的逻辑 * * Class Test * @package app */ class Test { // do something before // do something // do something after public function show() { return 'Hello World'; } public function say() { return 'I Can Fly'; } }
ProxyVisitor.php
<?php /** * Created by PhpStorm. * User: CSH * Date: 2019/4/4 * Time: 11:16 */ namespace App\Aop; 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; use PhpParser\NodeVisitorAbstract; /** * PHP Parser在Aop编程中的使用 * * 流程: * 1、当我们拿到节点是类时,我们重置这个类,让新建的类继承这个类。 * 2、当我们拿到的节点是类方法时,我们使用proxyCall来重写方法。 * 3、当遍历完成之后,给类加上我们定义好的AopTrait。 * * NodeVisitor接口调用顺序:beforeTraverse -> enterNode -> leaveNode -> afterTraverse * * Class ProxyVisitor * @package App\Aop */ class ProxyVisitor extends NodeVisitorAbstract { protected $className; protected $proxyId; public function __construct($className, $proxyId) { $this->className = $className; $this->proxyId = $proxyId; } public function getProxyClassName() { 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')]); } /** * Called when leaving a node * 把类方法里的逻辑重置掉 * * @param Node $node * @return int|null|Node|Node[]|Class_|ClassMethod */ 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, ]); } } /** * Called once after traversal * 把AopTrait扔到类里 * * @param array $nodes * @return array|null|Node[] */ 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 * @package 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; } }
AopController.php
<?php namespace App\Http\Controllers; use PhpParser\ParserFactory; use PhpParser\NodeDumper; use PhpParser\NodeTraverser; use App\Aop\ProxyVisitor; use PhpParser\PrettyPrinter\Standard; class AopController extends Controller { public function index() { $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); $ast = $parser->parse(file_get_contents(base_path().'/app/Test.php')); // 把parser代码后的语法树(对象)转为字符串形式 // $dumper = new NodeDumper(); // dd($dumper->dump($ast)); $className = 'App\\Test'; $proxyId = uniqid(); $visitor = new ProxyVisitor($className, $proxyId); $traverser = new NodeTraverser(); $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); // dd($proxyCode); eval($proxyCode); $class = $visitor->getClassName(); $bean = new $class(); echo $bean->show(); } }