当前位置: 代码网 > it编程>编程语言>Php > PHP parser重写PHP类使用示例详解

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

2024年05月18日 Php 我要评论
引言最近一直在研究 swoft 框架,框架核心当然是 aop 切面编程,所以想把这部分的心得记下来,以供后期查阅。swoft 新版的 aop 设计建立在 php parser 上面。所以这片文章,主要

引言

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com