PHPでPHPコードのエラーを調べる

(1/1)
初心者がPHPプログラミング学習するとき、関数名のスペルミスなどでエラーが出て、プログラムを動くようにできるまで時間がかかる、という声を多く聞く。PHPの組み込み関数はC言語に比べて名前が長く数量も多いことから、専用IDEを使っていないとスペルミスすることも多いだろう。
そこで今回は、PHPでPHPコードをチェックするツールを作ってみることにする。VSCodePHPStanPhpStorm などの構文チェックツールがあるが、初心者が導入するにはハードルが高いので、PHPプログラム1本で完結できるツールで、最低限、構文エラーと関数名のミスをチェックできるプログラムを目指す。

目次

サンプル・プログラムの実行例

PHPで巨大素数の生成と判定

サンプル・プログラム

圧縮ファイルの内容
checkPHPcode.phpサンプル・プログラム
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
checkPHPcode.php 更新履歴
バージョン 更新日 内容
1.0.0 2023/05/07 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.5.0 2024/01/28 exitIfExceedVersion() 追加
1.4.2 2024/01/28 exitIfLessVersion() メッセージ修正
1.4.1 2023/09/30 コメントの訂正
1.4.0 2023/09/09 $_GET, $_POST参照をfilter_input()関数に置換
1.3.0 2023/07/11 roundFloat() 追加

PHPコード解析クラス

PHPコード解析のためのメソッドをユーザー定義クラス pahooAnalizePHPcode にまとめた。

 170: //PHPコード解析クラス =======================================================
 171: 
 172: //解析モード
 173: define('PAP_LINT', 1);              //lintによりチェックを行う
 174: define('PAP_UNDEFINED', 2);         //未定義をチェックする
 175: define('PAP_DEFINED', 4);           //二重定義をチェックする
 176: 
 177: class pahooAnalizePHPcode {
 178:     var $code;              //PHPコード
 179:     var $tokens;            //トークンに分離したPHPコード
 180:     var $constantList;      //ユーザー定義定数一覧
 181:     var $functionList;      //ユーザー定義関数一覧
 182:     var $classList;         //ユーザー定義クラス一覧
 183:     var $errors;            //エラー一覧
 184:     var $errmsg;            //エラーメッセージ
 185:     var $mode;              //解析モード
 186: 
 187: 
 188: /**
 189:  * コンストラクタ
 190:  * @param   string $code  解析したいPHPコード
 191:  * @param   int    $mode  解析モード(省略可能)
 192:  * @return  なし
 193: */
 194: function __construct($code, $mode=PAP_LINT | PAP_UNDEFINED) {
 195:     $this->code         = $code;
 196:     $this->constantList = array();
 197:     $this->functionList = array();
 198:     $this->classList    = array();
 199:     $this->errors       = array();
 200:     $this->errmsg = '';
 201:     $this->mode = $mode;
 202:     $this->tokens = PhpToken::tokenize($this->code);    //トークンに分離
 203: }
 204: 
 205: /**
 206:  * デストラクタ

コンストラクタにPHPコード渡すことで、後述する PhpTokenクラスを使ってトークン(字句要素)に分解する。また、第2引数(省略可能)によって解析モードを指定することができる。現時点では、UI側から明示的に解析モードを指定できるようにはしていないが、将来的に複雑な解析を実装したときの準備として用意した。
コードチェックをクラスに分離したことで、1つのPHPプログラムの中で複数のPHPファイルのチェックを可能としている。

PhpTokenクラスとコード解析

PHPに用意されているPhpTokenクラスは、PHPがプログラムを実行するときに使う [Zend engine:blue の字句解析スキャナによってPHPコードを字句要素に分解する。

 264: /**
 265:  * PHPコードを解析し,エラーを配列に格納する.
 266:  * ユーザー定義定数/クラス/関数も配列に格納する.
 267:  * @param   なし
 268:  * @return  なし
 269: */
 270: function analyze() {
 271:     //すべての定義済みクラスを取得しておく.
 272:     $allClasses = get_declared_classes();
 273:     //すべての定義済み関数を取得しておく.
 274:     $allFunctions = get_defined_functions();
 275:     //すべての定義済み定数を取得しておく.
 276:     $allConstants = get_defined_constants(TRUE);
 277: 
 278:     $flag = $mode = '';
 279:     $php = (-1);
 280:     foreach ($this->tokens as $token) {
 281:         switch ($token->id) {
 282:             //PHPコード開始
 283:             case T_OPEN_TAG:
 284:                 $php = 1;
 285:                 break;
 286:             //PHPコード終了
 287:             case T_CLOSE_TAG:
 288:                 $php--;
 289:                 break;
 290:             //ユーザー定義定数の場合
 291:             case T_CONSTANT_ENCAPSED_STRING:
 292:                 if ($mode == T_CONSTANT_ENCAPSED_STRING) {
 293:                     $ss = preg_replace('/[\'\"]/ui', '', $token->text);
 294:                     //新規の定数
 295:                     if (array_search($ss, $this->constantList) == FALSE) {
 296:                         array_push($this->constantList, $ss);
 297:                         $this->unsetError($token->id, $ss);
 298:                     //定数の二重定義
 299:                     } else if ($this->mode & PAP_DEFINED) {
 300:                         $arr['line'] = $token->line;
 301:                         $arr['id']   = T_CONSTANT_ENCAPSED_STRING;
 302:                         $arr['word'] = $ss;
 303:                         $arr['text'] = '二重定義';
 304:                         array_push($this->errors, $arr);
 305:                     }
 306:                     $mode = '';
 307:                 }
 308:                 break;
 309:             //class または function
 310:             case T_CLASS:
 311:             case T_FUNCTION:
 312:             case T_OBJECT_OPERATOR:
 313:             case T_DOUBLE_COLON:
 314:                 $flag = $token->id;
 315:                 break;
 316:             case T_STRING:
 317:                 $ss = $token->text;
 318:                 //ユーザー定義定数の場合
 319:                 if (preg_match('/define/ui', $ss> 0) {
 320:                     $mode = T_CONSTANT_ENCAPSED_STRING;
 321:                 //ユーザー定義関数の場合
 322:                 } else if ($flag == T_FUNCTION) {
 323:                     $this->unsetError($flag, $ss);
 324:                     array_push($this->functionList, $ss);
 325:                 //ユーザー定義クラスの場合
 326:                 } else if ($flag == T_CLASS) {
 327:                     $this->unsetError($flag, $ss);
 328:                     array_push($this->classList, $ss);
 329:                 //オブジェクト
 330:                 } else if ($flag == T_OBJECT_OPERATOR) {
 331:                 //オブジェクト
 332:                 } else if ($flag == T_DOUBLE_COLON) {
 333:                 //定数、関数、クラスが未定義かどうか
 334:                 } else {
 335:                     if (in_array($ss, $this->constantList)
 336:                         || in_array($ss, $this->functionList)
 337:                         || in_array($ss, $this->classList)
 338:                         || in_array($ss, $allFunctions['internal'])
 339:                         || in_array($ss, $allClasses)
 340:                         ) {
 341:                     } else {
 342:                         $ff = FALSE;
 343:                         foreach ($allConstants as $key1=>$arr1) {
 344:                             if ($key1 == 'user')    continue;
 345:                             if ($ff)    break;
 346:                             foreach ($arr1 as $key2=>$va2) {
 347:                                 if ($ff) {
 348:                                     break;
 349:                                 } else if ($key2 == $ss) {
 350:                                     $ff = TRUE;
 351:                                     break;
 352:                                 }
 353:                             }
 354:                         }
 355:                         if (($this->mode & PAP_UNDEFINED&& ($ff == FALSE)) {
 356:                             $arr['line'] = $token->line;
 357:                             $arr['id']   = $token->id;
 358:                             $arr['word'] = $ss;
 359:                             $arr['text'] = '未定義';
 360:                             array_push($this->errors, $arr);
 361:                         }
 362:                     }
 363:                 }
 364:                 $flag = '';
 365:                 break;
 366:         }
 367:     }
 368: 
 369:     //PHP開始コード/終了コードを調べる.
 370:     if ($php < 0) {
 371:         $arr['line'] = 0;
 372:         $arr['id']   = 0;
 373:         $arr['word'] = '';
 374:         $arr['text'] = 'PHPコードではないか,PHP開始タグと終了タグの組み合わせが不正';
 375:         array_push($this->errors, $arr);
 376:     }
 377: }

PhpTokenクラスを使ったトークンの分解は pahooAnalizePHPcode のコンストラクタで行う。ユーザーメソッド analyze メソッドの中で、個々のトークンから下記の解析を行う。
  1. PHP開始コード/終了コードが存在するかどうか。
  2. ユーザー定義定数は定義済みか,二重定義していないかどうか。
  3. ユーザー定義関数は定義済みかどうか。
  4. ユーザー定義クラスは定義済みかどうか。
  5. これらが未定義の場合は,定義済み定数/組込関数/定義済みクラスにあるかどうか。
関数名の綴りミスがあると、これらをチェックを行うことで、T_STRINGぶ分類されたトークンが「未定義」というステータスになり、冒頭の目的を達成できる。

定義済み定数かどうかは組み込み関数  get_defined_constants  を、定義済み関数かどうかは組み込み関数  get_defined_functions  を、定義済みクラスかどうかは組み込み関数  get_declared_classes  を利用した。厳密には、 get_declared_classes  はPHP処理系に定義されているクラスだけでなく、本プログラムでユーザー定義した pahooAnalizePHPcode も含んでしまうのだが、大勢に影響はしないと考え、そのままにしている。

ユーザーが定義した定数は配列(pahooAnalizePHPcodeのプロパティ、以下同様) $constantList に、ユーザーが定義した関数は配列 $functionList に、ユーザーが定義した関数は配列 $$classList に格納していく。
定数/関数/クラスが呼び出される際に、定義済み、ないしは配列に存在していなければ「未定義」としてエラー配列 $errros に  array_push  する。

 231: /**
 232:  * 配列から定数/関数/クラスを未定義エラーを取り除く.
 233:  * @param   int    $id 
 234:  * @param   string $word 
 235:  * @return  なし
 236: */
 237: function unsetError($id, $word) {
 238:     switch ($id) {
 239:         case T_CONSTANT_ENCAPSED_STRING:
 240:         case T_CLASS:
 241:         case T_FUNCTION:
 242:             foreach ($this->errors as $key=>$val) {
 243:                 if (($val['id'] == T_CONSTANT_ENCAPSED_STRING&& ($val['word'] == $word)) {
 244:                     unlink($this->errors[$key]);
 245:                 }
 246:             }
 247:             break;
 248:     }
 249: }

その後に定義されていることが分かったら、メソッド unsetError を使ってエラー配列から取り除く。

lintコマンド

PHP実行環境(Windowsでは php.exe)は -lオプションを指定することで、指定したファイルの文法解析のみを行う。これを lintコマンドと呼ぶ。
静的解析のみであり、実際にプログラムを動かしたときに発生する動的なエラーはチェックできない、最初に表れるエラー1件のみしかチェックできないなどの制約がある。

 379: /**
 380:  * lintコマンドの実行結果を求める.
 381:  * @param   なし
 382:  * @return  string エラーメッセージ/FALSE:linkコマンド実行エラー
 383: */
 384: function execLint() {
 385:     //一時ファイルを生成する.
 386:     $path = sys_get_temp_dir();
 387:     $tmpfname = tempnam($path, 'pahoge');
 388:     $result = file_put_contents($tmpfname, $this->code);
 389:     if ($result !== FALSE) {
 390:         $handle = popen(LINT . $tmpfname, 'r');
 391:         if ($handle !== FALSE) {
 392:             $result = '';
 393:             while (! feof($handle)) {
 394:                 $result .fgets($handle);
 395:             }
 396:             $result = trim($result);
 397:             pclose($handle);
 398:         }
 399:         //一時ファイルを削除する.
 400:         unlink($tmpfname);
 401:     }
 402:     return $result;
 403: }

メソッド execLint は、lintコマンドを実行し、メッセージを返す。lintコマンドに実行プログラムは、定数 LINT に定義しておく。

 405: /**
 406:  * lintコマンドの実行結果をエラーに代入する.
 407:  * @param   なし
 408:  * @return  bool FALSE:エラーがある/TRUE:エラーがない,または非実行
 409: */
 410: function lint2error() {
 411:     $result = TRUE;
 412: 
 413:     //lintコマンドを実行する必要がなければ終了.
 414:     if (($this->mode | PAP_LINT) == 0)  return $result;
 415: 
 416:     //lintコマンドを実行する.
 417:     $str = $this->execLint();
 418:     if ($str == FALSE)      return FALSE;
 419: 
 420:     //エラーがある場合.
 421:     if ((preg_match('/(.+)in\s+.+on(\s+)line\s+([0-9]+)/ui', $str, $arr> 0)
 422:         || (preg_match('/(.+)(in|on)\s+line\s+([0-9]+)/ui', $str, $arr> 0)
 423:     ) {
 424:         $errmsg = trim($arr[1]);
 425:         //メッセージ冒頭の 'Parse error:' を取り除く.
 426:         $errmsg = preg_replace('/^Parse\s+error\s*\:/ui', '', $errmsg);
 427:         //エラー配列に代入する.
 428:         $arr1['line'] = (int)$arr[3];
 429:         $arr1['id']   = 0;
 430:         $arr1['word'] = '';
 431:         $arr1['text'] = trim($errmsg);
 432:         array_push($this->errors, $arr1);
 433:         $result = FALSE;
 434:     }
 435: 
 436:     return $result;
 437: }

次にメソッド lint2error を使い、lintコマンドのメッセージをエラー配列 $errors に追加する。
メッセージは英語で初心者にとっては読みにくいかもしれない。いずれ、「PHPでクラウド翻訳サービスを利用する」を参考に、自動和訳できるようにしようと思う。

UIについて

UI部分にJavaScriptを用いている。
ファイルのドロップは「解説:ファイルのドロップ - PHPで撮影場所をマッピング」を、クリップボードへのコピーは、「JavaScriptでクリップボードを使う」を、それぞれご覧いただきたい。

参考サイト

(この項おわり)
header