開発ツール・パッケージ PHP 正規表現 静的解析

はじめに

PHPソースコードからクラス名やメソッド情報を抽出したい場合、正規表現で解析できます。ドキュメント自動生成やコード解析ツールの下ごしらえに便利です。この記事では、名前空間・クラス名・メソッド・PHPDocを正規表現で取り出す実装を示し、あわせてPHP 8.x以降の戻り値型・型付き引数・finalreadonlyといった新しい記法に対応させるポイントも解説します。

先に結論を書くと、正規表現は「ざっくり抽出」には十分ですが、修飾子の順序やネストした型には破綻しやすい点に注意が必要です。最後に触れるように、厳密さが要るならnikic/php-parserへ切り替えるのが現実的です。

クラス名と名前空間の抽出

$content = file_get_contents($filePath);

// 名前空間
preg_match('/namespaces+([w\\]+)s*;/', $content, $nsMatch);
$namespace = $nsMatch[1] ?? '';

// クラス名
preg_match('/^s*(?:abstracts+)?classs+(w+)/m', $content, $classMatch);
$className = $classMatch[1] ?? '';

PHP 8.2以降ではfinal classreadonly class、さらにenuminterfacetraitも宣言できます。これらもまとめて拾いたい場合は、修飾子と種別を柔軟に許容する次のパターンが使えます。

// final / abstract / readonly を任意の順で許容し、種別も判定する
$pattern = '/^s*(?:(?:final|abstract|readonly)s+)*(class|interface|trait|enum)s+(w+)/m';
preg_match($pattern, $content, $typeMatch);
$structureType = $typeMatch[1] ?? '';  // class, interface, trait, enum
$className = $typeMatch[2] ?? '';

メソッドの抽出

PHP 8.x以降は引数に型宣言、メソッドに戻り値型を書くのが一般的です。従来の可視性だけを見るパターンではfinalabstract付きのメソッドを取りこぼすため、修飾子を柔軟に受け付ける形へ更新します。

$pattern = '/(?:(?:final|abstract)s+)*(public|protected|private)s+(statics+)?functions+(w+)s*(([^)]*))(?:s*:s*(??[w\\|&]+))?/';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

foreach ($matches as $match) {
    $visibility = $match[1];        // public, protected, private
    $isStatic = !empty($match[2]);
    $methodName = $match[3];
    $params = $match[4];            // 型付き引数もそのまま入る
    $returnType = $match[5] ?? '';  // ?int, string, User|null, A&B など
}

末尾の(?:s*:s*(??[w\\|&]+))?が戻り値型を捕捉する部分です。?intのようなnullable型、User|nullのユニオン型、Countable&Traversableのインターセクション型(PHP 8.1〜)まで拾えます。ただし()内にデフォルト値としてカッコを含む引数(例:new Foo())があると[^)]*が途中で切れるため、その場合は後述のパーサ利用を検討してください。

PHPDocコメントの抽出

メソッド宣言の直前にあるPHPDocを取得するには、オフセットを使います。

preg_match_all($pattern, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);

foreach ($matches as $match) {
    $methodOffset = $match[0][1];

    // メソッド宣言の前300文字を取得
    $before = substr($content, max(0, $methodOffset - 300), 300);

    // PHPDocブロックを抽出
    if (preg_match('//**[sS]*?*/s*$/s', $before, $docMatch)) {
        $docBlock = $docMatch[0];
    }
}

PHP 8.0で導入された属性(Attributes)を使っているコードでは、PHPDocとメソッド宣言の間に#[Route(...)]のような行が挟まります。前300文字という固定幅では属性が長いとDocを取りこぼすことがあるため、余裕を持たせるか、属性行を除外してから照合すると安定します。

まとめ

正規表現を使えばPHPソースから構造情報を抽出でき、戻り値型や型付き引数、finalreadonlyといったPHP 8.x以降の記法もパターン次第で拾えます。一方で、デフォルト値のカッコ・属性・複雑な型のネストには弱く、限界があります。より正確な解析が必要な場合はnikic/php-parserを使い、ASTベースで扱うことを検討してください。

カスタマイズに関するお問い合わせはこちら