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