当前位置: 代码网 > 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-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com