はじめに
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 | $flags — TOKEN_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_ECHO や T_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_* 定数の存在確認に使えることがある |
よくある落とし穴(注意点)
- 戻り値が配列と文字列の混在である
is_array()でのチェックを忘れると、単一文字トークン(;や(など)を処理する際にインデックスアクセスでエラーになります。 <?phpタグを省略してはいけないtoken_get_all()に渡す文字列には開始タグを含める必要があります。タグなしで渡すと正しくトークン化されず、T_INLINE_HTMLとして丸ごと1トークンになってしまいます。- 空白・改行もトークンとして扱われる
T_WHITESPACEを除外し忘れると、次のトークンとの隣接判定(例4のような処理)が誤動作します。 TOKEN_PARSEフラグの有無で結果が変わる場合がある 予約語がプロパティ名やメソッド名として使われている場合など、文脈依存の解釈が必要なケースではTOKEN_PARSEを付けないと期待通りのトークン種別にならないことがあります。- 文法エラーがあるコードは正しくトークン化できないことがある
token_get_all()は構文的に不完全なコードに対しても部分的にトークンを返そうとしますが、PHPのバージョンによって挙動が異なるため、構文エラーを含むコードの解析には注意が必要です。 PhpToken::tokenize()はPHP 8.0未満では使用不可 後方互換性が必要なプロジェクトでは、手続き型のtoken_get_all()を使い続ける必要があります。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | PHPコードをトークン(字句解析の最小単位)の配列に分解する |
| 主な用途 | 静的解析、Linter、コードフォーマッター、変数/関数名の抽出など |
| 戻り値の形式 | 文字列(単一文字トークン)または [ID, テキスト, 行番号] の配列が混在 |
| OOP代替 | PHP 8.0以降は PhpToken::tokenize() が利用可能 |
| 注意点 | タグの省略不可、空白・コメントの扱い、TOKEN_PARSEフラグの影響 |
token_get_all() は地味に見える関数ですが、コード解析ツールを自作する上での土台になる非常に強力な機能です。ぜひ手元のプロジェクトで、簡単なコード解析スクリプトを書いて挙動を確かめてみてください。
