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类的资料请关注脚本之家其它相关文章!

相关文章

  • PHP将URL转换成短网址的算法分享

    PHP将URL转换成短网址的算法分享

    短网址(Short URL)顾名思义就是在形式上比较短的网址。在Web 2.0的今天,不得不说这是一个潮流。目前已经有许多类似服务,借助短网址您可以用简短的网址替代原来冗长的网址,让使用者可以更容易的分享链接,下面来看看如何用PHP实现这个功能,有需要的朋友们可以参考。
    2016-09-09
  • php中关于socket的系列函数总结

    php中关于socket的系列函数总结

    这篇文章主要介绍了php中关于socket的系列函数总结,本文列举了所有关于PHP语言中使用socket相关服务的一些函数,需要的朋友可以参考下
    2015-05-05
  • CI框架集成Smarty的方法分析

    CI框架集成Smarty的方法分析

    这篇文章主要介绍了CI框架集成Smarty的方法,对比分析并改进了网上已有的方法,详细介绍了CI框架继承Smarty模板的具体步骤与相关技巧,需要的朋友可以参考下
    2016-05-05
  • 浅谈PHP检查数组中是否存在某个值 in_array 函数

    浅谈PHP检查数组中是否存在某个值 in_array 函数

    下面小编就为大家带来一篇浅谈PHP检查数组中是否存在某个值 in_array 函数。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • php面试中关于面向对象的相关问题

    php面试中关于面向对象的相关问题

    在本篇文章里小编整理了关于php面试中关于面向对象的相关问题内容,以及相关回答,有需要的朋友们学习下。
    2019-02-02
  • ThinkPHP框架实现用户信息查询更新及删除功能示例

    ThinkPHP框架实现用户信息查询更新及删除功能示例

    这篇文章主要介绍了ThinkPHP框架实现用户信息查询更新及删除功能,结合实例形式分析了thinkPHP框架数据库配置、控制与模板调用实现信息查询、更新、删除等功能相关操作技巧,需要的朋友可以参考下
    2018-03-03
  • openai createChatCompletion函数使用实例

    openai createChatCompletion函数使用实例

    这篇文章主要为大家介绍了openai createChatCompletion函数使用实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • TP5.0框架实现无限极回复功能的方法分析

    TP5.0框架实现无限极回复功能的方法分析

    这篇文章主要介绍了TP5.0框架实现无限极回复功能的方法,结合实例形式分析了thinkPHP5.0框架下无限极回复功能相关的数据库、评论功能及界面布局实现方法,需要的朋友可以参考下
    2019-05-05
  • Zend Framework框架中实现Ajax的方法示例

    Zend Framework框架中实现Ajax的方法示例

    这篇文章主要介绍了Zend Framework框架中实现Ajax的方法,结合实例形式详细分析了Zend Framework框架中实现ajax功能的具体步骤与相关操作技巧,需要的朋友可以参考下
    2017-06-06
  • CI框架AR数据库操作常用函数总结

    CI框架AR数据库操作常用函数总结

    这篇文章主要介绍了CI框架AR数据库操作常用函数,结合实例形式总结分析了基于CI框架的数据库增删改查与缓存、结果集等相关操作函数与技巧,需要的朋友可以参考下
    2016-11-11

最新评论