插件窝 干货文章 PHP parser重写PHP类使用示例详解

PHP parser重写PHP类使用示例详解

class Node PhpParser 我们 579    来源:    2024-10-29

引言

最近一直在研究 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类的资料请关注插件窝其它相关文章!