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年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

总结的很棒 赞

5年前 评论

感谢分享 学习了

5年前 评论

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