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有四个方法,其中

  1. beforeTraverse()方法用于遍历之前,通常用来在遍历前对值进行重置。
  2. afterTraverse()方法和(1)相同,唯一不同的地方是遍历之后才触发。
  3. 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 协议》,转载必须注明作者和本文链接
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
本帖由系统于 4年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3

总结的很棒 赞

5年前 评论

感谢分享 学习了

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!