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:
print_r(token_get_all('<?php echo "Hello Weblabor!"; ?>'));
kimenete ez lesz:
Array
 (
 [0] => Array
     (
         [0] => 354
         [1] => < ?php
     )

 [1] => Array
     (
         [0] => 314
         [1] => echo
     )

 [2] => Array
     (
         [0] => 357
         [1] =>
     )

 [3] => Array
     (
         [0] => 313
         [1] => "Hello Weblabor!"
     )

 [4] => ;
 [5] => Array
     (
         [0] => 357
         [1] =>
     )

 [6] => Array
     (
         [0] => 356
         [1] => ? >
     )
 )
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:
<?php
token_name(354) == T_OPEN_TAG
token_name(314) == T_ECHO
?>

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:
    <?php
    function __highlight_string($str) {
      print '<pre>';
      $tokens = token_get_all($str);
      while(list(, $token) = each($tokens)) {
        if (is_string($token)) {
          print '<span class="string">' . $token . '</span>';
        }
        else {
          print '<span class="' . token_name($token[0]) . '">' . $token[1] . '</span>';
        }
      }
      print '</pre>';
    }
    ?>
    Ezzel a függvénnyel kódszínezett forráskód könnyen testreszabható stíluslappal, pl. megadható, hogy a
    .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:
    <?php
    function varnames($str) {
      $tokens = token_get_all($str);
      while (list(, $token) = each($tokens)) {
        if ($token[0] == T_VARIABLE) {
          $return[substr($token[1], 1)] = TRUE;
        }
      }
      return(array_flip($return));
    }?>
    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:
    <?php
    function replace_consts($str) {
      $tokens = token_get_all($str);
    
      while (list($i, $token) = each($tokens)) {
        if (is_string($token)) {
          print $token;
        }
        else {
          if ($token[0] == T_STRING
             && $tokens[$i + 1] !== '('
             && defined($token[1])) {
            var_export(constant($token[1]));
          }
          else {
            print $token[1];
          }
        }
      }
    }
    ?>
    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:
    <?php replace_consts('<?php echo M_PI;?>'); ?>
    eredménye ez lesz:
    <?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:
<?php
while(list($i, $token) = each($tokens))
?>
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:
; : , . [ ] ( ) | ^ & + - / * = % ! ~$ < > ? @
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):
<?php

// Akkor lássuk az egyszerűbb, egykarakteres tokeneket:
if (is_string($token)) {

  switch($token) {
    
    // Általában a pontosvesszők és kettőspontok után szokott lenni
    // újsor karakter, tegyünk mi is azon melegében és csináljunk igazítást
    // is, mivel egy újabb programsorhoz érkeztünk el.
    case ';':
    case ':':
      print $token . chr(10) . str_repeat(' ', $indent);
           break;
    
    // A nyitó- és záró kapcsos zárójelek után is szokott lenni újsor, de
    // ezeket külön kell kezelni, mivel egy "blokk" (pl. függvény definíció)
    // kezdetét/végét jelölik, ezért nemcsak újsor karakter kell, hanem az
    // igazítás mértékét is manipulálni kell:
    case '{':
      print $token . chr(10) . str_repeat(' ', ++$indent);
      break;
    case '}':
      print $token . chr(10) . str_repeat(' ', --$indent);
      break;
    
    // Ezzel meg is volnánk, csak a maradék többi karakterről
    // kell gondoskodni; azaz csak kiíratni őket és lezárni a
    // switch-blokkot:
    default:
      print $token;
      break;
  }
}

// Jöjjenek hát a bonyolultabb fajta tokenek, vizsgáljuk csak
// a token fajtáját.
else {
    
  switch($token[0]) {
    
    // Újsort és igazítás teszünk a nyitó PHP tagek elé, ill. hogy a PHP
    // kód is beljebb legyen igazítva teszünk igazítást is:
    case T_OPEN_TAG:
      print '< ?php' . chr(10) . str_repeat(' ', ++$indent);
      break;
    
    // Még azt szeretnénk elérni, hogy az értékadások (T_*_EQUAL) és
    // értékvizsgálatok (T_*_IDENTICAL) elé és után is tegyünk egy
    // szóközt, hogy szép $változó = érték; formátumhoz jussunk
    case T_AND_EQUAL:
    case T_BOOLEAN_AND:
    case T_BOOLEAN_OR:
    case T_CONCAT_EQUAL:
    case T_DIV_EQUAL:
    case T_DOUBLE_ARROW:
    case T_IS_EQUAL:
    case T_IS_GREATER_OR_EQUAL:
    case T_IS_IDENTICAL:
    case T_IS_NOT_EQUAL:
    case T_IS_NOT_IDENTICAL:
    case T_LOGICAL_OR:
    case T_LOGICAL_XOR:
    case T_LOGICAL_AND:
    case T_IS_SMALLER_OR_EQUAL:
    case T_MINUS_EQUAL:
    case T_OR_EQUAL:
    case T_PLUS_EQUAL:
    case T_SL:
    case T_SL_EQUAL:
    case T_SR:
    case T_SR_EQUAL:
    case T_XOR_EQUAL:
      // Itt felsoroltuk az összes értékadás típust a teljesség kedvéért, node
      // mit is kell csinálni? Kiírni egy szóközt, a token értékét (legyen az
      // >> vagy .=) majd megint egy szóköz, hogy szép legyen
      print ' ' . $token[1] . ' ';
      break;
    
    // A többi tokennel peddig itt nem foglalkozunk, azok nyugodtan mehetnek
    // a kimenetre és zárjuk le a switchet is
    default:
      print $token[1];
      break;
  }

}
?>
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-