在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 AppAop; use PhpParserNode; use PhpParserNodeExprClosure; use PhpParserNodeExprFuncCall; use PhpParserNodeExprMethodCall; use PhpParserNodeExprVariable; use PhpParserNodeName; use PhpParserNodeParam; use PhpParserNodeScalarString_; use PhpParserNodeStmtClass_; use PhpParserNodeStmtClassMethod; use PhpParserNodeStmtReturn_; use PhpParserNodeStmtTraitUse; use PhpParserNodeFinder; use PhpParserNodeVisitorAbstract; /** * PHP Parser在Aop编程中的使用 * * 流程: * 1、当我们拿到节点是类时,我们重置这个类,让新建的类继承这个类。 * 2、当我们拿到的节点是类方法时,我们使用proxyCall来重写方法。 * 3、当遍历完成之后,给类加上我们定义好的AopTrait。 * * NodeVisitor接口调用顺序:beforeTraverse -> enterNode -> leaveNode -> afterTraverse * * Class ProxyVisitor * @package AppAop */ 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 PhpParserNodeStmtTraitUse */ private function getAopTraitUseNode(): TraitUse { // Use AopTrait trait use node return new TraitUse([new Name('AppAopAopTrait')]); } /** * 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 AppAop */ 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 AppHttpControllers; use PhpParserParserFactory; use PhpParserNodeDumper; use PhpParserNodeTraverser; use AppAopProxyVisitor; use PhpParserPrettyPrinterStandard; 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(); } }
参考:
https://learnku.com/articles/14387/aop-design-rewrite-the-php-class-using-php-parser