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