How to Automatically Generate PHP code with PHP-Parser
PHP:8.0
nikic/PHP-Parser: 4.12.0
在大型專案中,因為架構極其複雜,所以常會看到有許多由抽象類別、介面實作出來同性質的子物件,比如說 Parser、Extractor 等等。而當依據需求實作出大量又有些微客製化的物件後,如果要一次性調整大量的物件程式碼,比方說修改特定 Variable 的值,就會非常耗費人力。
在這種情況下,我們可以透過 nikic/PHP-Parser
套件達成自動產生 PHP code 的效果。
nikic/PHP-Parser 是一套針對 PHP 設計的 Abstract Syntax Tree(抽象語法樹) 解析工具,除了可以將 PHP code 解析成 AST
之外,也可以透過 AST
產生 PHP code。
Installation
nikic/PHP-Parser
透過 Composer
即可安裝。
php composer.phar require nikic/php-parser
Basic Usage
nikic/PHP-Parser
套件可以分成三個重點:
- Parse PHP code to
AST
- Visit
AST
- Parse
AST
to PHP code
這裡舉官方提供的範例,透過實作以上三點,達到動態修改 PHP Function
的功能。
Parse PHP code to AST
require 'vendor/autoload.php';
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
// PHP demo function code
// 這裡的 test function 會執行 var_dump($foo)
$code = <<<'CODE'
<?php
function test($foo)
{
var_dump($foo);
}
CODE;
// 建立 PHP 7 ParserFactory Parser
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
// 將 PHP code 轉成 AST
$ast = $parser->parse($code);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return;
}
// Dump 出 AST 結構的字串
// 也可以利用 print_r($ast) 去看各節點物件
$dumper = new NodeDumper;
echo $dumper->dump($ast) . PHP_EOL;
Visit AST
require 'vendor/autoload.php';
use PhpParser\Node;
use PhpParser\NodeDumper;
use PhpParser\Node\Stmt\Function_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
// 產生走訪 AST 節點的物件
$traverser = new NodeTraverser();
// 實作走訪時的邏輯(代入 Anonymous Class)
$traverser->addVisitor(new class extends NodeVisitorAbstract {
public function enterNode(Node $node) {
// 遇到是 Function 的節點,就將 Function 內容清空
if ($node instanceof Function_) {
$node->stmts = [];
}
}
});
// 代入原 AST 開始走訪,產生新的 AST
$ast = $traverser->traverse($ast);
// Dump 出 AST 結構
$dumper = new NodeDumper;
echo $dumper->dump($ast) . PHP_EOL;
Parse AST
to PHP code
require 'vendor/autoload.php';
use PhpParser\PrettyPrinter;
// 產生輸出 PHP code 的物件
$prettyPrinter = new PrettyPrinter\Standard;
// 將 AST 轉換成 PHP code
echo $prettyPrinter->prettyPrintFile($ast) . PHP_EOL;
Practical Usage
這邊再舉一個可能的應用情境,假設 Parser/
目錄底下有許多實作完成的 Parser
物件,Label01 ~ Label99,而需求是要在特定幾個 Parser
裡的 authorization
加入義大利的區域。
Label Parser 範例
namespace FallZuBallBall\Parser\Label01Parser;
class Label01Parser
{
protected $labelName = 'Label01';
protected $authorization = ['Taiwan' => 'enable', 'Japan' => 'disable'];
public function getMetaByAuth(): array
{
foreach ($this->authorization as $region => $status) {
// Do Something
}
}
}
AuthTool
透過 nikic/PHP-Parser
實作一個處理此需求的物件。
require_once 'vendor/autoload.php';
namespace FallZuBallBall;
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\PrettyPrinter;
class AuthTool
{
protected $ast;
// 設定要 Parse 的 PHP code
public function setFile($file): AuthTool
{
$code = file_get_contents($file);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$this->ast = $parser->parse($code);
} catch (Error $error) {
throw new Exception('Parse error:' . $error->getMessage());
}
return $this;
}
// 可以新增 Auth 資訊到 authorization
public function setAuth($region, $status): AuthTool
{
$traverser = new NodeTraverser();
// 將傳入的變數代入 Anonymous Class
$traverser->addVisitor(new class($region, $status) extends NodeVisitorAbstract {
protected $region;
protected $status;
public function __construct($region, $status) {
$this->region = $region;
$this->status = $status;
}
public function enterNode(Node $node) {
// 尋找到 PropertyProperty 節點
if ($node instanceof \PhpParser\Node\Stmt\PropertyProperty) {
// 判斷是否為 authorization
if ($node->name->name === 'authorization') {
// 加入新的 Auth
$item = new ArrayItem(new String_($this->status), new String_($this->region));
$node->default->items[] = $item;
}
}
}
});
$this->ast = $traverser->traverse($this->ast);
return $this;
}
public function dumpCode(): string
{
$prettyPrinter = new PrettyPrinter\Standard;
return $prettyPrinter->prettyPrintFile($this->ast);
}
}
執行
$obj = new AuthTool;
$code = $obj->setFile('Parser/Label01Parser.php')
->setAuth('Italy', 'disable')
->dumpCode();
print_r($code);
產生的結果
namespace FallZuBallBall\Parser\Label01Parser;
class Label01Parser
{
protected $labelName = 'Label01';
protected $authorization = ['Taiwan' => 'enable', 'Japan' => 'disable', 'Italy' => 'disable'];
public function getMetaByAuth() : array
{
foreach ($this->authorization as $region => $status) {
// Do Something
}
}
}