最近一直在研究Swoft框架,框架核心當然是Aop切面程式設計,所以想把這部分的心得記下來,以供後期查閱。
Swoft新版的Aop設計建立在PHP Parser上面。所以這片文章,主要介紹一下PHP Parser在Aop程式設計中的使用。
簡單的來講,我們想在某些類的方法上進行埋點,比如下面的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,我只是個懶惰的搬運工,有興趣的可以去看一下。
本作品採用《CC 協議》,轉載必須註明作者和本文連結