[PHP]token_get_allとは?PHPソースコードをトークン単位に分解する仕組みを徹底解説

PHP

はじめに

PHPで書かれたコードそのものを「解析対象のデータ」として扱いたい場面はないでしょうか。たとえば静的解析ツール、コードフォーマッター、独自のLintツール、あるいはコード中の特定のパターンを検出するツールなどを自作するとき、PHPのソースコードを文字列としてただ眺めるのではなく、意味のある単位(トークン)に分解して扱いたくなります。

そこで登場するのが token_get_all() 関数です。この関数はPHPの内部パーサーが使っているのと同じ字句解析(レキサー)の仕組みを利用して、PHPコードをトークンの配列に変換してくれます。本記事では、token_get_all() の基本的な使い方から、実務で使える7つの実践的なサンプルコード、そしてよくある落とし穴まで、じっくり解説していきます。


関数概要

項目内容
関数名token_get_all()
所属拡張Tokenizer(標準で有効)
シグネチャtoken_get_all(string $code, int $flags = 0): array
引数1$code — 解析対象のPHPソースコード(<?php タグを含む文字列)
引数2$flagsTOKEN_PARSE を指定可能(省略時は0)
戻り値トークンを表す配列。各要素は「文字列」または [トークンID, テキスト, 行番号] の配列
対応バージョンPHP 4以降(TOKEN_PARSE はPHP 7.0以降)
OOP版の有無あり。PHP 8.0以降は PhpToken::tokenize() が推奨される

トークン化の流れ(イメージ図)

PHPコードは以下のような流れでトークンに分解されます。

入力コード
  "<?php echo 'Hello';"
          │
          ▼
   ┌─────────────────┐
   │  token_get_all() │   ← PHP内部の字句解析器を呼び出す
   └─────────────────┘
          │
          ▼
  トークン配列に変換
  ┌───────────────────────────────────────┐
  │ [T_OPEN_TAG,   "<?php ", 1]           │
  │ [T_ECHO,       "echo",   1]           │
  │ [T_WHITESPACE, " ",      1]           │
  │ [T_CONSTANT_ENCAPSED_STRING,"'Hello'",1]│
  │ ";"  ← 単一文字トークンは文字列のまま  │
  └───────────────────────────────────────┘

ポイントは、すべてのトークンが配列になるわけではない という点です。;( のような「単一文字トークン」は、そのまま1文字の文字列として返されます。一方、T_ECHOT_STRING のように名前が付いた複合トークンは [トークンID, テキスト, 行番号] の3要素配列として返されます。この非対称性が、初心者がつまずきやすいポイントの一つです。


実践サンプル7選

以下では、クラスベースの実装を交えながら token_get_all() の実用的な使い方を7パターン紹介します。

例1:基本的なトークン化

<?php

class BasicTokenizer
{
    public function tokenize(string $code): array
    {
        // token_get_all() はPHPの開始タグを含む文字列を要求する
        return token_get_all($code);
    }
}

$tokenizer = new BasicTokenizer();
$tokens = $tokenizer->tokenize("<?php \$x = 1 + 2;");

foreach ($tokens as $token) {
    if (is_array($token)) {
        // 配列トークンの場合、token_name()で名前を取得できる
        echo token_name($token[0]) . " => " . var_export($token[1], true) . PHP_EOL;
    } else {
        // 単一文字トークンはそのまま文字列
        echo "単一文字トークン => '{$token}'" . PHP_EOL;
    }
}

例2:コード中の変数名だけを抽出する

<?php

class VariableExtractor
{
    /**
     * ソースコード中に登場する変数名の一覧を重複なく取得する
     */
    public function extract(string $code): array
    {
        $variables = [];
        $tokens = token_get_all($code);

        foreach ($tokens as $token) {
            if (is_array($token) && $token[0] === T_VARIABLE) {
                // T_VARIABLEのテキストには "$" が含まれるので注意
                $variables[$token[1]] = true;
            }
        }

        return array_keys($variables);
    }
}

$extractor = new VariableExtractor();
$code = '<?php $name = "太郎"; $age = 20; echo $name . $age;';
print_r($extractor->extract($code));
// 出力例: ['$name', '$age']

例3:コメントを除去してコードを整形する

<?php

class CommentStripper
{
    /**
     * // や /* */ 、# コメントを取り除いたコードを返す
     */
    public function strip(string $code): string
    {
        $result = '';
        $tokens = token_get_all($code);

        foreach ($tokens as $token) {
            if (is_array($token)) {
                // T_COMMENTとT_DOC_COMMENTを除外
                if ($token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) {
                    continue;
                }
                $result .= $token[1];
            } else {
                $result .= $token;
            }
        }

        return $result;
    }
}

$stripper = new CommentStripper();
$code = "<?php\n// これはコメント\n\$x = 1; /* 複数行\nコメント */\n";
echo $stripper->strip($code);

例4:関数呼び出しの一覧をカウントする

<?php

class FunctionCallCounter
{
    /**
     * T_STRING の直後に "(" が来る箇所を関数呼び出しとみなして集計する
     */
    public function count(string $code): array
    {
        $tokens = token_get_all($code);
        $counts = [];
        $total = count($tokens);

        for ($i = 0; $i < $total; $i++) {
            $token = $tokens[$i];

            if (is_array($token) && $token[0] === T_STRING) {
                // 次のトークン(空白を飛ばす)を確認
                $next = $this->findNextMeaningful($tokens, $i + 1);
                if ($next === '(') {
                    $name = $token[1];
                    $counts[$name] = ($counts[$name] ?? 0) + 1;
                }
            }
        }

        return $counts;
    }

    private function findNextMeaningful(array $tokens, int $start)
    {
        for ($i = $start; $i < count($tokens); $i++) {
            $t = $tokens[$i];
            if (is_array($t) && $t[0] === T_WHITESPACE) {
                continue;
            }
            return is_array($t) ? $t[1] : $t;
        }
        return null;
    }
}

$counter = new FunctionCallCounter();
$code = '<?php strlen("abc"); strlen($x); trim(" y ");';
print_r($counter->count($code));
// 出力例: ['strlen' => 2, 'trim' => 1]

例5:TOKEN_PARSEフラグの活用(予約語の扱いの違いを確認)

<?php

class ReservedWordChecker
{
    /**
     * TOKEN_PARSE フラグを使うと、文脈に応じて予約語が
     * より正確なトークン種別として扱われる
     */
    public function compare(string $code): array
    {
        $withoutFlag = token_get_all($code);
        $withFlag = token_get_all($code, TOKEN_PARSE);

        return [
            'without_flag' => $this->summarize($withoutFlag),
            'with_flag'    => $this->summarize($withFlag),
        ];
    }

    private function summarize(array $tokens): array
    {
        $names = [];
        foreach ($tokens as $token) {
            if (is_array($token)) {
                $names[] = token_name($token[0]);
            }
        }
        return $names;
    }
}

$checker = new ReservedWordChecker();
// classプロパティ名としての"list"などが対象になるケースで違いが出やすい
$result = $checker->compare('<?php class Foo { const list = 1; }');
print_r($result);

例6:行番号を使ってエラー位置を特定するリンター風ツール

<?php

class SimpleLinter
{
    /**
     * 短縮タグ <? の使用を検出し、行番号付きで警告を返す
     */
    public function lint(string $code): array
    {
        $warnings = [];
        $tokens = token_get_all($code);

        foreach ($tokens as $token) {
            if (is_array($token) && $token[0] === T_OPEN_TAG) {
                // 短縮オープンタグかどうかをテキストで判定
                if (trim($token[1]) === '<?' ) {
                    $warnings[] = sprintf(
                        '%d行目: 短縮オープンタグは非推奨です',
                        $token[2]
                    );
                }
            }
        }

        return $warnings;
    }
}

$linter = new SimpleLinter();
$code = "<?php\n\$a = 1;\n?>\n<?\n\$b = 2;\n";
print_r($linter->lint($code));

例7:PhpToken::tokenize()を使ったOOP版の同等処理(PHP 8.0以降)

<?php

class OopTokenizer
{
    /**
     * PHP 8.0以降ではPhpTokenクラスを使うことで
     * オブジェクト指向スタイルでトークンを扱える
     */
    public function tokenize(string $code): array
    {
        $tokens = PhpToken::tokenize($code);
        $summary = [];

        foreach ($tokens as $token) {
            // $token->isIgnorable() で空白・コメントを判定可能
            if ($token->isIgnorable()) {
                continue;
            }
            $summary[] = [
                'name' => $token->getTokenName(),
                'text' => $token->text,
                'line' => $token->line,
            ];
        }

        return $summary;
    }
}

$tokenizer = new OopTokenizer();
print_r($tokenizer->tokenize("<?php \$x = 1;"));

関連関数との比較

関数/クラス役割token_get_allとの違い
token_get_all()ソースコードをトークン配列に変換(手続き型)本記事の対象。配列/文字列混在の戻り値
PhpToken::tokenize()同上(OOP版、PHP 8.0以降)各トークンが PhpToken オブジェクトになり、isIgnorable() などのメソッドが使える
token_name()トークンIDから定数名の文字列を取得token_get_all() の結果を人間が読める形にするための補助関数
PhpToken::getTokenName()同上(OOPインスタンスメソッド)token_name() のOOP版
get_defined_constants()定義済み定数の一覧を取得トークン化とは無関係だが、T_* 定数の存在確認に使えることがある

よくある落とし穴(注意点)

  1. 戻り値が配列と文字列の混在である is_array() でのチェックを忘れると、単一文字トークン(;( など)を処理する際にインデックスアクセスでエラーになります。
  2. <?php タグを省略してはいけない token_get_all() に渡す文字列には開始タグを含める必要があります。タグなしで渡すと正しくトークン化されず、T_INLINE_HTML として丸ごと1トークンになってしまいます。
  3. 空白・改行もトークンとして扱われる T_WHITESPACE を除外し忘れると、次のトークンとの隣接判定(例4のような処理)が誤動作します。
  4. TOKEN_PARSE フラグの有無で結果が変わる場合がある 予約語がプロパティ名やメソッド名として使われている場合など、文脈依存の解釈が必要なケースでは TOKEN_PARSE を付けないと期待通りのトークン種別にならないことがあります。
  5. 文法エラーがあるコードは正しくトークン化できないことがある token_get_all() は構文的に不完全なコードに対しても部分的にトークンを返そうとしますが、PHPのバージョンによって挙動が異なるため、構文エラーを含むコードの解析には注意が必要です。
  6. PhpToken::tokenize() はPHP 8.0未満では使用不可 後方互換性が必要なプロジェクトでは、手続き型の token_get_all() を使い続ける必要があります。

まとめ

観点まとめ
何をする関数かPHPコードをトークン(字句解析の最小単位)の配列に分解する
主な用途静的解析、Linter、コードフォーマッター、変数/関数名の抽出など
戻り値の形式文字列(単一文字トークン)または [ID, テキスト, 行番号] の配列が混在
OOP代替PHP 8.0以降は PhpToken::tokenize() が利用可能
注意点タグの省略不可、空白・コメントの扱い、TOKEN_PARSEフラグの影響

token_get_all() は地味に見える関数ですが、コード解析ツールを自作する上での土台になる非常に強力な機能です。ぜひ手元のプロジェクトで、簡単なコード解析スクリプトを書いて挙動を確かめてみてください。

タイトルとURLをコピーしました