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

 更新时间:2023年09月08日 14:05:15   作者:李铭昕  
这篇文章主要为大家介绍了PHP parser重写PHP类使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

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

相关文章

  • cakephp常见知识点汇总

    cakephp常见知识点汇总

    这篇文章主要介绍了cakephp常见知识点,汇总整理了cakephp的模板、数据库、日志、表单等相关操作技巧,需要的朋友可以参考下
    2017-02-02
  • php 解决旧系统 查出所有数据分页的类

    php 解决旧系统 查出所有数据分页的类

    不同之处在于 没有实现分页的系统, 默认全部查出来 现在就要不能动后台的基础上进行操作 可以采用 相应的 如下 代码
    2012-08-08
  • PHP如何初始化PDO及原始SQL语句操作

    PHP如何初始化PDO及原始SQL语句操作

    PDO 已经是 PHP 中操作数据库事实上的标准。包括现在的框架和各种类库,都是以 PDO 作为数据库的连接方式。基本上只有我们自己在写简单的测试代码或者小的功能时会使用 mysqli 来操作数据库。注意,普通的 mysql 扩展已经过时了哦!
    2021-06-06
  • 详解HTTP Cookie状态管理机制

    详解HTTP Cookie状态管理机制

    cookie 最早是网景公司的雇员 Lou Montulli 在1993年3月发明,后被 W3C 采纳,目前 cookie 已经成为标准,所有的主流浏览器如 IE、Chrome、Firefox、Opera 等都支持
    2016-01-01
  • PHP环境搭建的详细步骤

    PHP环境搭建的详细步骤

    这篇文章主要为大家介绍了PHP环境搭建的详细步骤,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • Zend Framework教程之Zend_Db_Table表关联实例详解

    Zend Framework教程之Zend_Db_Table表关联实例详解

    这篇文章主要介绍了Zend Framework教程之Zend_Db_Table表关联用法,结合实例形式较为详细的分析了Zend_Db_Table表关联的定义,实现方法与相关注意事项,需要的朋友可以参考下
    2016-03-03
  • PHP之预定义接口详解

    PHP之预定义接口详解

    这篇文章主要整理了PHP之预定义接口,在平时项目过程中比较常用的四个接口:IteratorAggregate(聚合式aggregate迭代器Iterator)、Countable、ArrayAccess、Iterator,需要的朋友可以参考下
    2015-07-07
  • php中curl使用指南

    php中curl使用指南

    这篇文章主要介绍了php中curl使用指南,十分详细,需要的朋友可以参考下
    2015-02-02
  • laravel按天、按小时,查询数据的实例

    laravel按天、按小时,查询数据的实例

    今天小编就为大家分享一篇laravel按天、按小时,查询数据的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-10-10
  • PHP 将逗号、空格、回车分隔的字符串转换为数组的函数

    PHP 将逗号、空格、回车分隔的字符串转换为数组的函数

    我们在搜索一些东西时会经常遇到可以通过空格隔开来达到输入多个条件的目的。今天正好项目中遇到了这个情况,就写了一个函数,将多个条件放到数组里
    2012-06-06

最新评论