靜態分析、代碼審計、文檔自動生成、重構工具等場景常需從 PHP 源文件中**提取所有函數定義** (函數名、參數列表、函數體)。挑戰在於:函數體包含巢狀大括號 {}、字符串/註釋中的大括號干擾、多行格式不固定。
php_strip_whitespace() 移除註釋、空白、換行,**保留字符串字面量**,獲得單行壓縮代碼,極大簡化正則難度。(?R) 或 (?1) 遞歸引用子模式,可精確匹配平衡的巢狀大括號。<?php
/**
* 從 PHP 文件提取所有函數定義
* @param string $filePath
* @return array [ ['name'=>..., 'params'=>..., 'body'=>..., 'start'=>..., 'end'=>...], ... ]
*/
function extractFunctions(string $filePath): array {
$code = php_strip_whitespace($filePath);
if ($code === '') return [];
// 正則模式 (x 修飾符忽略空白、允許註釋)
$pattern = <<<'REGEX'
~
\b function \s+ # function 關鍵字
(?P [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* ) # 函數名
\s* \( (?P [^)]* ) \) \s* # 參數列表 (簡略:不處理參數中的括號,實際需遞歸)
(?P \{ (?: [^{}]++ | (?&body) )* \} ) # 函數體:遞歸匹配平衡大括號
~x
REGEX;
$flags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
if (!preg_match_all($pattern, $code, $matches, $flags)) {
return [];
}
$functions = [];
foreach ($matches as $m) {
$functions[] = [
'name' => $m['name'][0],
'params' => $m['params'][0],
'body' => $m['body'][0],
'start' => $m['name'][1], // 在壓縮代碼中的偏移
'end' => $m['name'][1] + strlen($m[0][0]),
];
}
return $functions;
}
// 測試
$testCode = <<<'PHP'
10) {
return $sum * 2;
}
return $sum;
}
function foo() { echo "bar"; }
PHP;
file_put_contents('/tmp/test.php', $testCode);
print_r(extractFunctions('/tmp/test.php'));
(?P \{ (?: [^{}]++ | (?&body) )* \} )
# 解讀:
# \{ 匹配左大括號
# (?: 非捕獲組
# [^{}]++ 佔有量詞:匹配任意非大括號字符 (不回溯)
# | 或
# (?&body) 遞歸調用名為 body 的子模式 (即自身)
# )*
# \} 匹配右大括號
# 整體匹配一對平衡的大括號及其內容,支持任意層級巢狀。
| 問題 | 原因 | 解決方案 |
|---|---|---|
參數列表中含默認值數組/函數調用 func([1,2]) 導致提前截斷 | 模式 [^)]* 不處理巢狀括號 | 參數部分同樣使用遞歸模式 (?P |
箭頭函數 fn($x) => $x+1 無大括號體 | 模式強制要求 {...} | 增加分支匹配 fn\s*\(...\)\s*=>\s*[^;]+ |
| 類方法 (public/protected/private/static/abstract/final) | 模式未匹配修飾符 | 在 function 前加 (?: \b (?:public|protected|private|static|abstract|final) \b \s+ )* |
命名空間、use 引入、屬性 (PHP 8 #[Attr]) | 干擾函數識別 | 預處理階段移除或模式增加容忍 |
Heredoc/Nowdoc 中包含 function 關鍵字 | php_strip_whitespace 保留字符串內容 | 字符串內不會有真實函數定義,誤報率極低;嚴謹場景需詞法分析器 |
正則終究是「正則」,無法 100% 覆蓋 PHP 複雜語法。**業界標準做法**是使用 **PHP-Parser** 將源碼解析為 AST (抽象語法樹),再遍歷節點提取信息:
composer require nikic/php-parser
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\ClassMethod;
$code = file_get_contents('src/Service.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// 可選:解析名稱解析 (use 導入、命名空間)
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$ast = $traverser->traverse($ast);
// 遍歷提取
$functions = [];
foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($ast)) as $node) {
if ($node instanceof Function_ || $node instanceof ClassMethod) {
$functions[] = [
'name' => $node->name->toString(),
'params' => array_map(fn($p) => $p->toString(), $node->params),
'returnType' => $node->returnType?->toString(),
'body' => $node->stmts ? $node->stmts : [],
'isStatic' => $node->isStatic(),
'visibility' => $node->getVisibility(),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
];
}
}
print_r($functions);
優勢:100% 語法正確、支持所有 PHP 版本特性、可獲取精確行號、易於擴展分析邏輯 (如參數類型、返回類型、屬性、DocBlock)。
php_strip_whitespace + PCRE 遞歸模式可快速實現「函數體提取」原型,適合輕量級腳本、CI 檢查、簡單重構。面對生產級代碼庫、**複雜語法 (PHP 8.1+)、需精確行號與類型信息**的場景,**請直接使用 PHP-Parser**,以 AST 為基礎構建分析工具,避免正則維護地獄。