サンプル・プログラムの実行例
サンプル・プログラム
checkPHPcode.php | サンプル・プログラム |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
1.0.0 | 2023/05/07 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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コード解析クラス
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: * デストラクタ
コードチェックをクラスに分離したことで、1つのPHPプログラムの中で複数のPHPファイルのチェックを可能としている。
PhpTokenクラスとコード解析
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: }
- PHP開始コード/終了コードが存在するかどうか。
- ユーザー定義定数は定義済みか,二重定義していないかどうか。
- ユーザー定義関数は定義済みかどうか。
- ユーザー定義クラスは定義済みかどうか。
- これらが未定義の場合は,定義済み定数/組込関数/定義済みクラスにあるかどうか。
定義済み定数かどうかは組み込み関数 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: }
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: }
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: }
メッセージは英語で初心者にとっては読みにくいかもしれない。いずれ、「PHPでクラウド翻訳サービスを利用する」を参考に、自動和訳できるようにしようと思う。
UIについて
ファイルのドロップは「解説:ファイルのドロップ - PHPで撮影場所をマッピング」を、クリップボードへのコピーは、「JavaScriptでクリップボードを使う」を、それぞれご覧いただきたい。
参考サイト
- PhpTokenクラス:PHPマニュアル
- PHPの静的解析いろいろ:Qiita
そこで今回は、PHPでPHPコードをチェックするツールを作ってみることにする。VSCode、PHPStan、PhpStorm などの構文チェックツールがあるが、初心者が導入するにはハードルが高いので、PHPプログラム1本で完結できるツールで、最低限、構文エラーと関数名のミスをチェックできるプログラムを目指す。