PHP?parser重寫PHP類使用示例詳解
引言
最近一直在研究 Swoft 框架,框架核心當(dāng)然是 Aop 切面編程,所以想把這部分的心得記下來,以供后期查閱。
Swoft 新版的 Aop 設(shè)計(jì)建立在 PHP Parser 上面。所以這片文章,主要介紹一下 PHP Parser 在 Aop 編程中的使用。
Test 類
簡(jiǎn)單的來講,我們想在某些類的方法上進(jìn)行埋點(diǎn),比如下面的 Test 類。
class Test { public function get() { // do something } }
我們想讓它的 get 方法變成以下的樣子
class Test { public function get() { // do something before // do something // do something after } }
最簡(jiǎn)單的設(shè)計(jì)就是,我們使用 parser 生成對(duì)應(yīng)的語法樹,然后主動(dòng)修改方法體內(nèi)的邏輯。
接下來,我們就是用 PHP Parser 來搞定這件事。
首先我們先定一個(gè) ProxyVisitor
Visitor 有四個(gè)方法,其中
- beforeTraverse () 方法用于遍歷之前,通常用來在遍歷前對(duì)值進(jìn)行重置。
- afterTraverse () 方法和(1)相同,唯一不同的地方是遍歷之后才觸發(fā)。
- enterNode () 和 leaveNode () 方法在對(duì)每個(gè)節(jié)點(diǎn)訪問時(shí)觸發(fā)。
<?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,讓我們遍歷語法樹的時(shí)候,把類方法里的邏輯重置掉。另外就是重寫 afterTraverse 方法,讓我們遍歷結(jié)束之后,把我們的 AopTrait 扔到類里。AopTrait 就是我們賦予給類的,切面編程的能力。
創(chuàng)建一個(gè)測(cè)試類
首先,我們先創(chuàng)建一個(gè)測(cè)試類,來看看 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"; 結(jié)果樹如下 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 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ) ? ? ? ? ? ? ? ? ? ? ? ? ) ? ? ? ? ? ? ? ? ? ? ) ? ? ? ? ? ? ? ? ) ? ? ? ? ? ? ) ? ? ? ? ) ? ? ) )
語法樹的具體含義,我就不贅述了,感興趣的同學(xué)直接去看一下 PHP Parser 的文檔吧。(其實(shí)我也沒全都看完。。。大體知道而已,哈哈哈)
接下來重寫我們的 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); ? ? } }
當(dāng)我們拿到節(jié)點(diǎn)是類時(shí),我們重置這個(gè)類,讓新建的類繼承這個(gè)類。
當(dāng)我們拿到的節(jié)點(diǎn)是類方法時(shí),我們使用 proxyCall 來重寫方法。
當(dāng)遍歷完成之后,給類加上我們定義好的 AopTrait。
執(zhí)行
接下來,讓我們執(zhí)行以下第二個(gè) 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;
結(jié)果如下
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()); ? ? } }
這樣就很有趣了,我們可以賦予新建的類一個(gè)新的方法,比如 getOriginClassName。然后我們?cè)?proxyCall 中,就可以根據(jù) getOriginClassName 和 $method 拿到方法的精確 ID,在這基礎(chǔ)之上,我們可以做很多東西,比如實(shí)現(xiàn)一個(gè)方法緩存。
我這里呢,只給出一個(gè)最簡(jiǎn)單的示例,就是當(dāng)返回值為 string 的時(shí)候,加上個(gè)嘆號(hào)。
修改一下我們的代碼
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; ? ? } }
以及在我們的調(diào)用代碼后面加上以下代碼
eval($proxyCode); $class = $visitor->getClassName(); $bean = new $class(); echo $bean->show();
結(jié)果當(dāng)然和我們預(yù)想的那樣,打印出了
hello world!
以上設(shè)計(jì)來自 Swoft 開發(fā)組 swoft-component,我只是個(gè)懶惰的搬運(yùn)工,有興趣的可以去看一下。
以上就是PHP parser重寫PHP類使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于PHP parser重寫PHP類的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
PHP實(shí)現(xiàn)Session入庫/存入redis的方法
本篇文章主要介紹了PHP實(shí)現(xiàn)Session入庫/存入redis的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05PHP SPL標(biāo)準(zhǔn)庫之?dāng)?shù)據(jù)結(jié)構(gòu)棧(SplStack)介紹
這篇文章主要介紹了PHP SPL標(biāo)準(zhǔn)庫之?dāng)?shù)據(jù)結(jié)構(gòu)棧(SplStack)介紹,棧(Stack)是一種特殊的線性表,因?yàn)樗荒茉诰€性表的一端進(jìn)行插入或刪除元素(即進(jìn)棧和出棧),需要的朋友可以參考下2015-05-05php切割頁面div內(nèi)容的實(shí)現(xiàn)代碼分享
今天在百度知道看到一個(gè)關(guān)于php獲取DIV內(nèi)容的問題,做了一晚,終于是做出來了2012-07-07thinkPHP5.0框架獨(dú)立配置與動(dòng)態(tài)配置方法
這篇文章主要介紹了thinkPHP5.0框架獨(dú)立配置與動(dòng)態(tài)配置方法,結(jié)合實(shí)例形式分析了thinkPHP5.0框架獨(dú)立配置與靜態(tài)配置的功能、實(shí)現(xiàn)技巧與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-03-03PHP網(wǎng)頁游戲?qū)W習(xí)之Xnova(ogame)源碼解讀(十六)
這篇文章主要介紹了PHP網(wǎng)頁游戲Xnova(ogame)源碼解讀的攻擊任務(wù)頁面的代碼流程,需要的朋友可以參考下2014-06-06