前言#
最近在改之前寫的 PHP 小框架專案「AlpacaPHP」,路由的部分我希望能夠讓頁面可以自行在檔案中宣告要接受哪些 URI 參數,例如:
1Router::args($action, $id);
當然也可以不宣告表示沒有接收參數的需求。
我所預期的效果是,若檔案不需要參數,但使用者傳了,那路由就直接 drop 掉,而不是依舊導進頁面。
但實作中卻遇到一個阻礙 - include 前我怎麼知道該檔案是否需要參數?
路由在 include 檔案之前需要先知道檔案的參數要求;
想知道檔案的參數要求就需要先 include 它。
至此,流程成了套娃。
神奇的 Function 「token_get_all()」#
這個 function 可以理解成就是 PHP 內部的 Lexer (又稱 Scanner, Tokenizer)。
是用來將 PHP 程式碼標記成 Tokens 的程式,這意味著透過它,你能真正地知道檔案中有那些變數、functions。
你當然也可以自己寫一套流程解析檔案,但我想應該不會比 PHP 提供的 Lexer 來的更有效。
畢竟如果單純比對文字的話,那可能有註解內容被誤判的問題,因為你的程式不知道那是註解;但 token_get_all() 知道。
使用方法#
該 function 可以輸入程式碼文字,例如token_get_all('<?php echo $a;'),接著它會返回解析的 tokens,類型是陣列,內容大致如下:
1Array
2(
3 [0] => Array
4 (
5 [0] => 397 // T_OPEN_TAG
6 [1] => <?php // 原始程式字碼
7 [2] => 1 // 行號
8 )
9 [1] => Array
10 (
11 [0] => 328 // T_ECHO
12 [1] => echo
13 [2] => 1
14 )
15 [2] => Array
16 (
17 [0] => 393 // T_WHITESPACE (空格)
18 [1] =>
19 [2] => 1
20 )
21 [3] => Array
22 (
23 [0] => 320 // T_VARIABLE
24 [1] => $a
25 [2] => 1
26 )
27 [4] => ; // 簡單符號直接以字串呈現
28)
最外層每個 item 代表一個 token。
每個 token 的內容通常會有三個 items:
index 0:表示 token 的名稱。(但它是以整數表示,可以透過token_name()轉換成名稱字串,或者使用 PHP 的常數比對,例如320表示為 variable 的T_VARIABLE等)。index 1:表示原始的程式字碼。index 2:表示該 token 所在的行號。
回到剛剛說的,要判斷是否呼叫了 Router::args(),
我需要先判斷文字(T_STRING)「Router」;
接著判斷後面是否跟隨著雙冒號(T_DOUBLE_COLON)「::」;
再判斷後面是否又跟隨著文字(T_STRING)「args」;
最後判斷是否又跟隨著純字串「(」表示呼叫 function。
搭配 function file_get_contents() 讀取檔案,最終我的路由判斷方式如下:
1# magic function for check if file calls Router::args()
2static function getArgsCount($file) {
3 $code = file_get_contents($file);
4 $tokens = token_get_all($code);
5 $count = null;
6 for ($i = 0; $i < count($tokens); $i++) {
7 // 找到 Router::args(
8 if (
9 is_array($tokens[$i]) && $tokens[$i][0] === T_STRING && $tokens[$i][1] === 'Router' &&
10 isset($tokens[$i+1]) && $tokens[$i+1][0] === T_DOUBLE_COLON &&
11 isset($tokens[$i+2]) && $tokens[$i+2][0] === T_STRING && $tokens[$i+2][1] === 'args' &&
12 isset($tokens[$i+3]) && $tokens[$i+3] === '('
13 ) {
14 // 計算括號內的參數數量
15 $j = $i + 4;
16 $args = 0;
17 $parenLevel = 1;
18 while ($parenLevel > 0 && isset($tokens[$j])) {
19 if ($tokens[$j] === '(') $parenLevel++;
20 elseif ($tokens[$j] === ')') $parenLevel--;
21 elseif ($parenLevel === 1 && $tokens[$j] === ',') $args++;
22 $j++;
23 }
24 // 有參數才算
25 if ($j > $i + 4) $count = $args + ($j - $i - 4 > 1 ? 1 : 0);
26 break;
27 }
28 }
29 return $count;
30}
References#
- 《PHP: token_get_all - Manual》https://www.php.net/manual/en/function.token-get-all.php