ugrás a tartalomhoz

A PHP Tokenizer kiterjesztése

bbalint · 2004. Már. 30. (K), 23.00
A PHP Tokenizer kiterjesztése
A PHP fejlődése során egyre több belső lehetőséget tett elérhetővé a nyelvet használó programozók számára, gondolhatunk itt például a parse_ini_file() függvényre is, mely a php.ini feldolgozó kódot teszi a programozók számára is felhasználhatóvá.

Cikkemben a PHP Tokenizer kiterjesztéséről szeretnék néhány jó szót szólni, mely a PHP belső világát mutatja meg, a nyelvi feldolgozó használatát teszi lehetővé szkriptjeinkben. Szerintem ez az, amit (talán sokan is) kerestek. Írásom a haladóbbaknak szól, tehát aki nem látta még a PHP forráskódját illetve nem fordított PHP-t, annak lehet, hogy megfekszi a gyomrát, tehát csak óvatosan!

A sorozatban megjelent

Bevezető

A PHP-ben a 4.2.0-s változat óta létezik eme kiterjesztés, mely "alig" használható valamire. A 4.3.2-es verzióban a forráskódja 20kb és két függvényt definiál, a token_get_all()-t és a token_name()-et, illetve rengeteg (106) állandót. Ezt a kiterjesztést egy portál fejlesztésénél, XML-feldolgozásnál és egyáltalán sehol az életben nem igazán lehet felhasználni. Inkább amolyan programozói eszköz, de még milyen!

Tényleg, milyen?

A token_get_all() egy sztringet vár, melyet átad a Zend lexikális elemzőjének, majd annak "kimenetét" visszaadja egy tömbben. Például az alábbi kód:
  1. print_r(token_get_all('<?php echo "Hello Weblabor!"; ?>'));  
kimenete ez lesz:
  1. Array  
  2.  (  
  3.  [0] => Array  
  4.      (  
  5.          [0] => 354  
  6.          [1] => < ?php  
  7.      )  
  8.   
  9.  [1] => Array  
  10.      (  
  11.          [0] => 314  
  12.          [1] => echo  
  13.      )  
  14.   
  15.  [2] => Array  
  16.      (  
  17.          [0] => 357  
  18.          [1] =>  
  19.      )  
  20.   
  21.  [3] => Array  
  22.      (  
  23.          [0] => 313  
  24.          [1] => "Hello Weblabor!"  
  25.      )  
  26.   
  27.  [4] => ;  
  28.  [5] => Array  
  29.      (  
  30.          [0] => 357  
  31.          [1] =>  
  32.      )  
  33.   
  34.  [6] => Array  
  35.      (  
  36.          [0] => 356  
  37.          [1] => ? >  
  38.      )  
  39.  )  
A token_name() egy számot vár, és amennyiben van olyan értékű T_* állandó, akkor annak nevét adja vissza, például:
  1. <?php  
  2. token_name(354) == T_OPEN_TAG  
  3. token_name(314) == T_ECHO  
  4. ?>  

Mire jó?

Néhány érdekes lehetőség a tokenizer által nyílt utak közül:

  • Készíthetünk olyan szkripteket, melyek képesek futtatni más PHP fájlokat.
  • Fejleszthetünk a beépített highlight_string() és highlight_file() helyett saját célra jobban megfelelő megvalósítást:
    1. <?php  
    2. function __highlight_string($str) {  
    3.   print '<pre>';  
    4.   $tokens = token_get_all($str);  
    5.   while(list(, $token) = each($tokens)) {  
    6.     if (is_string($token)) {  
    7.       print '<span class="string">' . $token . '</span>';  
    8.     }  
    9.     else {  
    10.       print '<span class="' . token_name($token[0]) . '">' . $token[1] . '</span>';  
    11.     }  
    12.   }  
    13.   print '</pre>';  
    14. }  
    15. ?>  
    Ezzel a függvénnyel kódszínezett forráskód könnyen testreszabható stíluslappal, pl. megadható, hogy a
    1. .T_OPEN_TAG {color#f00;}  
    alkalmazásával a nyitó <?php, <?, <% tagek piros színben pompázzanak.
  • Kigyűjthető az összes használt változó/állandó/függvénynév egy fájlból. A változóneveket például így kell összeszedni:
    1. <?php  
    2. function varnames($str) {  
    3.   $tokens = token_get_all($str);  
    4.   while (list(, $token) = each($tokens)) {  
    5.     if ($token[0] == T_VARIABLE) {  
    6.       $return[substr($token[1], 1)] = TRUE;  
    7.     }  
    8.   }  
    9.   return(array_flip($return));  
    10. }?>  
    Ez a függvény egy tömbben fogja visszaadni a megadott kódban használt változók neveit (dollár-karakterek nélkül), és sokkal pontosabb, gyorsabb, mint egy reguláris kifejezés.
  • Persze, nemcsak ilyen statisztika jellegű gyűjtögető életmódra idomítható, hanem cseréket is elvégezhet akár:
    1. <?php  
    2. function replace_consts($str) {  
    3.   $tokens = token_get_all($str);  
    4.   
    5.   while (list($i$token) = each($tokens)) {  
    6.     if (is_string($token)) {  
    7.       print $token;  
    8.     }  
    9.     else {  
    10.       if ($token[0] == T_STRING  
    11.          && $tokens[$i + 1] !== '('  
    12.          && defined($token[1])) {  
    13.         var_export(constant($token[1]));  
    14.       }  
    15.       else {  
    16.         print $token[1];  
    17.       }  
    18.     }  
    19.   }  
    20. }  
    21. ?>  
    Ez a függvény a paraméterben kapott forráskódot írja ki, de ha abban szerepel valamilyen definiált állandó, akkor azt behelyettesíti annak értékére:
    1. <?php replace_consts('<?php echo M_PI;?>'); ?>  
    eredménye ez lesz:
    1. <?php echo 3.1415926535898;?>  
  • Megvalósíthatók "durvább" dolgok is, mindenkinek a saját fantáziájára bízom!

Hogyan?

Hogyan is kell valamit kezdeni ezzel a kiterjesztéssel? Nos, íme egy receptszerű dolog: "Hogyan csináljunk nagyon egyszerű kód szépítőt (code beautifier) rövid idő alatt?".

Hozzávalók:
  • PHP, tokenizer kiterjesztéssel;
  • programozó;
  • egy $indent változó melyben az igazítás mértékét tároljuk és kezdeti értéke
    nulla (nincs semmi beljebb-kezdés)
  • egy $tokens tömb, melyben a token_get_all() által kapott tokenek vannak

Elöljáróban, az igazítást szóközökkel, míg az $indent mennyiségű igazítást az str_repeat(' ', $indent) függvény értékének kiiratásával oldjuk meg.

Először is, kell egy ciklus, amivel végigmegyünk a kapott tokeneken:
  1. <?php  
  2. while(list($i$token) = each($tokens))  
  3. ?>  
Tökéletesen megfelel a célnak, sőt ha kell megnézhetjük az előző, következő, tegnapi, stb. tokent is egy $tokens[$i + 1] hivatkozással.

Most jön a feladat érdemi része, csinálni is kell valamit a tokenekkel. Alapvetően kétféle token van: Az egyik egyszerű karaktersorozat, valójában csak egy karakterből áll. Ezekkel találkozhatunk ilyenkor:
  1. ; : , . [ ] ( ) | ^ & + - / * = % ! ~$ < > ? @  
A másik bonyolultabb fajta, mely tömb. A tömb első eleme a token numerikus értéke. Ebből olvashatóbbat a token_name() függvénnyel csinálhatunk, de ez most nem szükséges. A tömb második eleme a token sztring értéke, pl. <?php, while, list, ?>

E rövid bevezető után lássuk a kódot (megjegyzésekkel segítve a megértését):
  1. <?php  
  2.   
  3. // Akkor lássuk az egyszerűbb, egykarakteres tokeneket:  
  4. if (is_string($token)) {  
  5.   
  6.   switch($token) {  
  7.       
  8.     // Általában a pontosvesszők és kettőspontok után szokott lenni  
  9.     // újsor karakter, tegyünk mi is azon melegében és csináljunk igazítást  
  10.     // is, mivel egy újabb programsorhoz érkeztünk el.  
  11.     case ';':  
  12.     case ':':  
  13.       print $token . chr(10) . str_repeat(' '$indent);  
  14.            break;  
  15.       
  16.     // A nyitó- és záró kapcsos zárójelek után is szokott lenni újsor, de  
  17.     // ezeket külön kell kezelni, mivel egy "blokk" (pl. függvény definíció)  
  18.     // kezdetét/végét jelölik, ezért nemcsak újsor karakter kell, hanem az  
  19.     // igazítás mértékét is manipulálni kell:  
  20.     case '{':  
  21.       print $token . chr(10) . str_repeat(' ', ++$indent);  
  22.       break;  
  23.     case '}':  
  24.       print $token . chr(10) . str_repeat(' ', --$indent);  
  25.       break;  
  26.       
  27.     // Ezzel meg is volnánk, csak a maradék többi karakterről  
  28.     // kell gondoskodni; azaz csak kiíratni őket és lezárni a  
  29.     // switch-blokkot:  
  30.     default:  
  31.       print $token;  
  32.       break;  
  33.   }  
  34. }  
  35.   
  36. // Jöjjenek hát a bonyolultabb fajta tokenek, vizsgáljuk csak  
  37. // a token fajtáját.  
  38. else {  
  39.       
  40.   switch($token[0]) {  
  41.       
  42.     // Újsort és igazítás teszünk a nyitó PHP tagek elé, ill. hogy a PHP  
  43.     // kód is beljebb legyen igazítva teszünk igazítást is:  
  44.     case T_OPEN_TAG:  
  45.       print '< ?php' . chr(10) . str_repeat(' ', ++$indent);  
  46.       break;  
  47.       
  48.     // Még azt szeretnénk elérni, hogy az értékadások (T_*_EQUAL) és  
  49.     // értékvizsgálatok (T_*_IDENTICAL) elé és után is tegyünk egy  
  50.     // szóközt, hogy szép $változó = érték; formátumhoz jussunk  
  51.     case T_AND_EQUAL:  
  52.     case T_BOOLEAN_AND:  
  53.     case T_BOOLEAN_OR:  
  54.     case T_CONCAT_EQUAL:  
  55.     case T_DIV_EQUAL:  
  56.     case T_DOUBLE_ARROW:  
  57.     case T_IS_EQUAL:  
  58.     case T_IS_GREATER_OR_EQUAL:  
  59.     case T_IS_IDENTICAL:  
  60.     case T_IS_NOT_EQUAL:  
  61.     case T_IS_NOT_IDENTICAL:  
  62.     case T_LOGICAL_OR:  
  63.     case T_LOGICAL_XOR:  
  64.     case T_LOGICAL_AND:  
  65.     case T_IS_SMALLER_OR_EQUAL:  
  66.     case T_MINUS_EQUAL:  
  67.     case T_OR_EQUAL:  
  68.     case T_PLUS_EQUAL:  
  69.     case T_SL:  
  70.     case T_SL_EQUAL:  
  71.     case T_SR:  
  72.     case T_SR_EQUAL:  
  73.     case T_XOR_EQUAL:  
  74.       // Itt felsoroltuk az összes értékadás típust a teljesség kedvéért, node  
  75.       // mit is kell csinálni? Kiírni egy szóközt, a token értékét (legyen az  
  76.       // >> vagy .=) majd megint egy szóköz, hogy szép legyen  
  77.       print ' ' . $token[1] . ' ';  
  78.       break;  
  79.       
  80.     // A többi tokennel peddig itt nem foglalkozunk, azok nyugodtan mehetnek  
  81.     // a kimenetre és zárjuk le a switchet is  
  82.     default:  
  83.       print $token[1];  
  84.       break;  
  85.   }  
  86.   
  87. }  
  88. ?>  
S ezzel meg is vagyunk egy PHP-kód szépítőszerrel, jó étvágyat!

Irodalom

Nem, nem a felhasznált birodalom, hanem amit érdemes nézegetni:
  • PHPdoksi: http://hu.php.net/manual/hu/
  • Tokenizer extension: http://hu.php.net/manual/hu/ref.tokenizer.php
  • Tokenek teljes listája: http://hu.php.net/manual/hu/tokens.php

Linkek

Kapcsolódó dolgok, amik biztos vannak. Erősen javasolt kereső kifejezés a Googlén: PHP obfuscator
 
bbalint
PHP-vel 2000 óta foglalkozik, köszönhetően az index.hu forráskódjának és maga kíváncsi természetének. Jelenleg egy reklámszervező cégnél dolgozik, mint (web)programozó és rendszergazda.
1

Google-s link nem jo

sajt · 2004. Már. 31. (Sze), 09.17
Rossz helyen van az kerdojel. (Egyebkent a cikk az tetszik.)
2

Re: Google-s link nem jo

Bártházi András · 2004. Már. 31. (Sze), 11.16
Linket javítottam. Nekem is azt hiszem az egyik kedvenc cikkem lesz. :)

-boogie-
3

Jó cikk, mindjárt ki is pr

laze · 2004. Ápr. 2. (P), 09.56
Jó cikk, mindjárt ki is próbálom...
Üdv: laze
4

valami rossz

bbalint · 2004. Ápr. 3. (Szo), 00.45
cső!

htpt://hu.php.net/manual/hu/tokens.php - itt egy apró helyesírási hiba van, a drupal/hozzászólás részében pedig fölösleges utf8_encode(), mivel az előző hozzászólás témája kicsit fura...

ami meg fura, hogy hiányoznak a kódrészletek a cikkből...
(pedig fontos lenne...)

no, jó éjt:

bbalint
6

Komment címek

Hojtsy Gábor · 2004. Ápr. 3. (Szo), 13.33
Ami a kommenteket illeti, nem teszteltük eléggé az automatikus komment cím adót, mert gondoltuk rájönnek a userek, hogy miért van a témának az üres mező (ez persze nem kifogás :)... A kutya ott van elásva, hogy az automatikus címadó nem veszi figyelembe az UTF-8 karaktereket reprezentáló bytesorozatokat, amiket így kettévág, és akkor lesz baj. Erre egy általános Drupal megoldás kell, ezért nyitottam rá egy bugreportot.
5

Re: valami rossz

Bártházi András · 2004. Ápr. 3. (Szo), 12.21
Szia!

Elgépelés javítva, a kódrészletek visszatéve ("kis" bug volt a rendszerben...)

-boogie-