Phar fájlok, avagy PHP archívumok

A Phar egy mozaik szó, ami a PHP Archive, azaz a PHP Archívum szavakból jött létre és egyben az archívum fájlok kiterjesztése is. Java programozók a JAR-hoz, illetve WAR-hoz hasonlíthatják. Bár bizonyos tekintetben a Windows DLL fájljaival is párhuzamot vonhatnánk, hiszen egy fájlba csomagolt programkönyvtárról beszélünk. A DLL-ekkel ellentétben viszont a PHAR fájlok futtathatók is, mint egy exe. Természetesen attól távol áll, mivel továbbra is a PHP értelmező lesz az, ami az értelmezést, futtatást végzi.

A PHAR kicsit mostoha gyerek, mert el is terjedt és nem is. Míg a JAR gyakorlatilag kéz a kézben jár a Java-val, a PHP archívumok nem ennyire általánosak és bizony gondolkodni kell rajta, hogy mikor, mire is érdemes használni, ha egyáltalán érdemes.

A PHP, fájlkiterjesztések és kódszervezés című cikkben kihagytam ezt a formátumot, ami lehetett volna szándékos, de szimplán elfelejtettem. Most viszont alaposabban körbejárom a témát. Igyekszem választ adni néhány potenciális kérdésre, illetve mutatok példákat és felhívom a figyelmet a buktatókra.

Abból, amit ez a formátum ígér, nem sikerült mindent kipróbálnom. A dokumentáció gyakran hiányos, és nem is beszélnek róla mások sem. Ettől függetlenül összeállítottam egy példasorozatot a főbb funkciók bemutatására a teljesség igénye nélkül. A példák elérhetők github-on vagy letölthetők Docker image-ként is "1.0" tag-et megadva.

Tartalomjegyzék

Phar kicsit közelebbről

A PHAR lényegében PHP fájlok egy becsomagolt változata, ahol van egy kitüntetett szerepű fájl, ami egyfajta belépési pont. Használható úgy is, mint egy hagyományos PHP fájl. Azaz include-dal betölthető egy másik fájlban és megnyitható böngészőben is, ha erre a webszerver fel van készítve. Ezek a programcsomagok aztán egyszerűbben hordozhatók, közzétehetők, hiszen nincs szükség be- és kitömörítésre. Ráadásul egy, a csomagban levő tetszőleges fájl külön is betölthető a "phar://" prefixet használva a csomag neve előtt. A PHP 5.3 óta pedig már az értelmezőbe beépített lehetőségről van szó. Ez eddig egy reklámszöveg is lehetne, amit sok helyen lehet olvasni, de azt már kevesebben mondják meg, hogy mikor is jó ez nekem valójában.

A PHP-s társadalom is fejlődhetett volna úgy, hogy ezek a csomagfájlok központibb szerepet kapjanak, de nem így történt. Java esetén egy jórészt eleve nem plain text formátumú fájlok összességéről beszélünk, azaz a PHP-hez képest nincs akkora különbség JAR és egy lefordított osztályokat tartalmazó mappa között. Egy PHP fejlesztő fejével gondolkodva viszont ez felér egy karunk elvesztésével. Nincs olyan, hogy valamit gyorsan kipróbálsz a forrásban vagy sürgős, kritikus hibajavítást végzel, mielőtt a főnök észreveszi a hatalmas bakit, vagy akár valaki kihasználja a sebezhetőséget. Más kérdés, hogy vannak veszélyei annak, ha az éles szerveren végzel módosítást és ezért ellenjavallt. Sőt, van, ahol lehetőség sincs rá, mert a fejlesztőnek nincs is hozzá joga. És az élesítés egy külön procedúra, ahol automatikus tesztek teszteket követnek, míg a változás megjelenik az éles környezetben. De legalábbis a fejlesztő aligha kontárkodhatna bele titokban.

Így aztán felmerül a kérdés, hogy a félelmeink oka a karunkhoz fűzött szoros viszony, vagy csak végtagjaink, lehetőségeink nem ismerete, az eszközök helytelen használata.

Ma már ezeket a fájlokat fejlesztés közben az IDE-k kibonthatják, olvashatják és még tesztelni, debugolni is lehet őket. Ennél viszont sokkal többet tudnak. A parancssoros verzióik már régóta elterjedtek, de lássuk, mi is van pontosan a formátum mögött és mik a további lehetőségek, mennyire éri meg foglalkozni vele.

Formátumok és felépítés

Ahogy már mondtam, a PHAR hasonló a JAR-hoz. Annyira, hogy mindkettő a ZIP formátumon alapul, bár a PHAR-nak van TAR verziója is. Akár el is készítheted a saját PHP archívumod egy mappában, amit betömörítesz ZIP-be vagy TAR-ba. Ennyi az egész. Ezen kívül van egy speciális PHAR formátum, ami nem hozható létre vagy bontható ki a hagyományos ZIP vagy TAR tömörítőkkel, viszont a PHAR php kiterjesztés nélkül is van lehetőség a futtatására.

Pontosabb összehasonlításért javaslom a hivatalos dokumentáció erre vonatkozó oldalát:
http://php.net/manual/en/phar.fileformat.comparison.php

Az archívumban speciális fájlok is lehetnek, mint a stub.php, ami a ".phar" nevű almappában van és nem lehet közvetlenül eléírni, viszont a phar fájl betöltésekor automatikusan lefut.

Itt lehet megoldani az automatikus osztálybetöltést vagy webalkalmazás esetén a routing-ot. Bizonyos metódusok pedig csak ebben a fájlban futtathatók.

Lehetőségek

A bevezetőben található link a github-os és Docker Hub-os verziókra, de ott csak angolul lehet róla olvasni, ezért nézzük itt is sorra a példákat magyarul.

A példákban a ".phar.php" kettős kiterjesztést használtam, hogy minél több szerveren kipróbálható legyen szerverkonfigurálás nélkül. Ez élesben nem feltétlenül szükséges.

A legegyszerűbb példa

[GitHub link]

Egy egyszerű, phpinfo() kimenetet megjelenítő alkalmazás néhány sorból elkészíthető. Ha az alkalmazás fájljai egy "src" nevű mappában vannak, akkor a mellette levő php fájlból az alábbi módon lehet futtatható archívumot készíteni, majd azt rögtön be is tölteni:

  1. <?php
  2.  
  3. $pharName = basename(__DIR__) . '.phar.php';
  4.  
  5. if (!file_exists($pharName)) {
  6.     $phar = new Phar($pharName);
  7.     $phar->setStub(Phar::createDefaultStub());
  8.     $phar->buildFromDirectory('src');
  9. }
  10.  
  11. require $pharName;

Szükség van egy Phar objektumra, ami konstruktorban megkapta a létrehozandó archívum helyét teljes névvel együtt. Kap egy alapértelmezett stub-ot, aminek a tartalmát a Phar::createDefaultStub() metódus hozza létre, majd a Phar::buildFromDirectory() metódus megadja, hogy az "src" mappa tartalmából készítsen egy archívumot.

Ehhez a php.ini-ben a "phar.readonly" opciót le kell tiltani, másképp a php nem tudja létrehozni a fájlokat, csak futtatni. Az eredmény aztán betölthető egy másik php szkriptből (require, include), futtatható a phar fájl parancssorból (php fajlneve.phar.php) vagy közvetlenül böngészőből is betölthető az elkészült fájl, amennyiben a szerverkonfiguráció ezt engedélyezi.

Ebben az esetben az src mappa csak egy index.php-t tartalmaz, ami a default stub-nak köszönhetően automatikusan lefut bármilyen módon is töltöd be a programot, majd megjeleníti a phpinfo-t. Az eredmény tehát ugyanaz lesz, amennyiben maga a kód nem tesz különbséget a különböző módok között. A phpinfo viszont html kimenetet ad webről megnyitva, és egyszerűbb szöveges tartalmat parancssorból.

Sajnos az alapértelmezett stub nem tökéletes, így gyakran szükség lehet saját megoldásra, de a legegyszerűbb esetekben megfelelő. Erről részletesebben majd később.

Több index fájl

[GitHub link]

Az előző példa minden esetben ugyanazt a kódot futtatta, ugyanazt az index fájlt töltötte be. A Phar osztály createDefaultStub metódusa viszont két opcionális paramétert is vár, amik közül első az alapértelmezett index fájl, második pedig az a fájl az archívumban, amire webről megnyitva kell irányítani a látogatót.

Valójában az, hogy webről érkezett-e a kérés olyan feltétel alapján dől el, ami nem garantáltan működik minden szerverbeállítással. Így például egy erre nem jól felkészített PHP FPM konfiguráció könnyen figyelmen kívül hagyhatja a default index fájlt vagy végtelen átirányításba kezdhet.

Jelenlegi tapasztalatok alapján a 2.4-es Apache HTTPD az alábbi sorral futattott FPM eredménye a végtelen átirányítás:

  1. ProxyPassMatch "^/(.*\.php)(/.*)?$" "unix:/tmp/php-fpm.sock|fcgi://127.0.0.1/var/www/"

A következővel viszont gond nélkül működik:

  1. <FilesMatch \.php$>
  2. SetHandler proxy:unix:/tmp/php-fpm.ock|fcgi://127.0.0.1
  3. </FilesMatch>
  4.    
  5. <Proxy "fcgi://127.0.0.1" enablereuse=on max=10>
  6. </Proxy>

Ráadásul a két különböző index ettől még ugyanúgy betölthető böngészőben, csak a megnyitandó fájl megadása nélkül a környezettől függő változatra irányít a szerver.

És a phar fájlt létrehozó szkript így néz ki:

  1. <?php
  2.  
  3. $pharName = basename(__DIR__) . '.phar.php';
  4.  
  5. if (!file_exists($pharName)) {
  6.     $phar = new Phar($pharName);
  7.     $phar->setStub(Phar::createDefaultStub('cli-index.php', 'web-index.php'));
  8.     $phar->buildFromDirectory('src');
  9. }
  10.  
  11. require $pharName;

A cli-index.php tartalmazza a phpinfo futtatását, a web-index.php pedig egy figyelmeztetést, mi szerint parancssorból kell futtatni a programot.

Több index fájl biztonságosan

[GitHub link]

Mivel a default stub nem védi meg a parancssorból futattandó fájlt, erről mindenképpen neked kell gondoskodni. Legegyszerűbben az alábbi cli-index.php-vel:

  1. <?php
  2. if (php_sapi_name() !== 'cli') {
  3.     exit('This script can run only from command line');
  4. }
  5.  

Itt, ha a php_sapi_name() függvény "cli"-t ad vissza, a program parancssorból fut. Minden más esetben hibaüzenet jelenhet meg.

Több index egyetlen fájl használatával

[GitHub link]

Az előző példában a createDefaultStub-ban megadott két index fájlnak nem sok értelme volt, mivel a cli-index.php-nek mindenképp vizsgálnia kellett a sapi értékét. Abban az esetben hasznos, amikor egy más által írt programnál, külön mappában, független programcsomag (pl "bin" almappa) tartalmazza a teljes parancssori kódot. Amikor viszont csak egy üzenetet kell megjeleníteni a weben, szükségtelen. A web-index.php tehát törölhető, a cli-index.php pedig átnevezhető index.php-re, és ismét vissza lehet térni a paraméter nélküli stub generálásra

  1. $phar->setStub(Phar::createDefaultStub());

Saját, egyedi stub fájl

[GitHub link]

Gyakran nincs szükség az alapértelmezett stub-ra és így a web, illetve cli megkülönböztetésére, vagy épp speciálisabb megoldásra van igény. Ilyenkor saját stub fájl készíthető. Nagyon fontos viszont, hogy ez a fájl az alábbi sorral kell, hogy végződjön:

  1. __HALT_COMPILER();

Így, nagybetűkkel kell írni, mert bár függvényre hasonlít, és azoknál a kis- és nagybetűk különbsége nem számít, másképpen a phar fájl nem fog megfelelően működni. A kódok készítésekor például közvetlenül lefordítás után betöltött a fájl, de a már létező fájlt utólag nem volt képes futtatni.

A fenti sor arra utasítja a php értelmezőt még a szintaktika elenőrzésekor, hogy az utána következő tartalmakat hagyja figyelmen kívül. Egy "exit()" is kilépne a programból futtatás közben, de egy szintaktikailag helytelen kódtól még el sem jutna a futtatásig az értelmező.

Erre azért van szükség, mert a phar fájlok a "stub.php" tartalmával kezdődnek. Utána viszont nem csak php kód szerepel, hanem metaadatok, css, js, de bármilyen, akár php fájl, ami lehet, soha nem fog futni. Még ha szintaktikailag hibás is. Hogy ezt mégis miért kell nekünk beírni a fájlba és miért nem kerül bele automatikusan, arra nem találtam választ.

Egy osztályokat betöltő stub fájl tehát így néz ki:

  1. <?php
  2.  
  3. spl_autoload_register(function ($class) {
  4.     $file = 'phar://' . __FILE__ . '/' . str_replace(['\\', '_'], '/', $class) . '.php';
  5.     if (is_file($file)) {
  6.         require $file;
  7.     }
  8. });
  9.  
  10. __HALT_COMPILER();

Itt látható, hogy a require utasítás egy "phar://" prefixszel kezdődő útvonalat kap, ami a __FILE__ konstanst tartalmazza a __DIR__ helyett az osztályokat tartalmazó mappa meghatározására. A "phar://" prefix szükséges, mert a stub.php nem az archívum része. Inkább csak egy fájl, ami automatikusan lefut a phar fájl betöltésekor. A phar fájlokból viszont csak a phar stream wrapper-rel lehet fájlokat kiolvasni. Másképp nézve viszont a stub fájl maga az archívum. Hiszen a kész, lefordított fájl annak kódjával kezdődik, majd leállítja az értelmezést. Így a __FILE__ konstans a phar fájl nevét fogja tartalmazni és pont erre van szükség, ha belőle egy fájlt szeretnék lekérni.

  1. require 'phar://phar-fajl-neve.phar.php/fajl/az/atchivumon/belul.php';

Na de hogy is készül ilyenkor az archívum:

  1. <?php
  2.  
  3. $pharName = basename(__DIR__) . '.phar.php';
  4.  
  5. if (!file_exists($pharName)) {
  6.     $phar = new Phar($pharName);
  7.     $phar->setStub(file_get_contents(__DIR__ . '/stub.php'));
  8.     $phar->buildFromDirectory('src');
  9. }
  10.  
  11. require $pharName;
  12.  
  13. $phpinfo = new \Rimelek\PharExample\Example05\PHPInfo(INFO_VARIABLES);
  14.  
  15. $phpinfo->run();

A PHPInfo osztály már a phar fájlból töltődik be automatikusan a stub.php-nek köszönhetően. Így működik tehát egy becsomagolt osztálykönyvtár.

Hol nem kell "phar://" prefix?

[GitHub link]

A "phar://" prefixre kizárólag a phar fájlon kívül, vagy a stub.php-ben van szükség. Amikor viszont a stub-ban betöltött fájlokban hivatkozol egy további php fájlra, a __FILE__ és __DIR__ konstansok már tartalmazzák a prefixet.

  1. // stub.php
  2. require_once 'phar://' . __FILE__ . '/autoload.php';
  3.  
  4. __HALT_COMPILER();
  1. // autoload.php
  2. spl_autoload_register(function ($class) {
  3.     $file = __DIR__ . '/' . str_replace(['\\', '_'], '/', $class) . '.php';
  4.     if (is_file($file)) {
  5.         require $file;
  6.     }
  7. });

Phar fájlon belüli és azon kívüli fájlok olvasása

[GitHub link]

Egyik sajátossága a phar fájloknak, hogy ha abban szövegfájlt, vagy akár json konfigurációs fájlt akarsz olvasni, ha arra relatív útvonallal hivatkozol, az a phar fájlhoz képest lesz relatív. Így pedig egy "file.txt" a phar fájl melletti és nem azon belüli fájlt fog jelenteni.

A Phar::interceptFileFuncs() metódus a stub.php-ben hívható csak, de ezt követően a stub által betöltött php fájlokban a relatív útvonalak az archívum gyökeréhez képest lesznek relatívak. Azaz a benne levő fájlra mutat a "file.txt" relatív útvonal. A stub.php továbbra is a külső szövegfájlt olvasná be, mivel ahogy azt korábban említettem, valójában nem része az archívumnak.

Ez a metódus tehát hasznos, ha egy más által írt programból kell csomagot készíteni, de mivel maximum az archívum gyökeréhez lehet relatívvá tenni a hivatkozásokat, nem a valódi futtatott fájlhoz képest, ez nem old meg minden problémát. Saját programokban egyébként sem szép relatívan hivatkozni. A __DIR__ konstans vagy egyedi konstans/változó használatával mindig pontosan olyan abszolút hivatkozást lehet készíteni, ami minden esetben a kívánt fájlra fog mutatni.

Álnevek

[GitHub link]

Korábbi példákban a __FILE__ konstanssal hivatkoztam a stub.php-ben az őt tartalmaző phar fájlra. Ez viszont csak a stub-ban működik. Máshol használható a Phar::running() metódusa, ami "phar://" prefix nélkül és azzal együtt is vissza tudja adni az archívum útvonalát. Viszont ez meg pont a stub fájlban használhatatlan, ahol nem ad vissza semmit. Van viszont még egy lehetőség. Ez pedig a Phar::mapPhar(), ami paraméterben azt a fájlnevet várja, amivel később szeretnél hivatkozni az archívumra. Valahogy így:

  1. // stub.php
  2. Phar::mapPhar('self.phar');
  3. // ...
  4. require 'phar://self.phar/path/inside/phar.php';

Talán érezhető, hogy enélkül is el lehetne boldogulni, de egyik előnye, hogy ezt az álnevet is lehet bármelyik fájlban használni függvényhívás nélkül, viszont könnyebb elgépelni is. A következő kódrészlet pedig nagyjából ugyanezt a célt szolgálná:

  1. // valahol az alkalmazás gyökér könyvtárában
  2. const BASEDIR = __DIR__;
  3.  
  4. // bárhol máshol
  5. require BASEDIR . '/path/inside/the/app.php';

Front controller webalkalmazásokhoz

[GitHub link]

A Phar::createDefaultStub() statikus metódussal generált stub fájl valójában a webPhar() metódust alkalmazza arra, hogy a webböngészőn keresztül érkező kéréseket másképpen szolgálja ki a program, mint a parancssori futtatás esetén. Webről ugyanis a webPhar() hívása utáni sorok nem futnak már le. A webPhar() elkapja a kérést és egy alapértelmezett routing-ot is megvalósít, azaz amit böngészőből megnyitva a phar fájl neve után írok egy / jelet követően, a phar fájlban annak megfelelő útvonalon levő php-t fogja lefuttatni.

  1. Phar::webPhar('self.phar');
  2.  
  3. echo 'Can you see me?';
  4.  
  5. __HALT_COMPILER();

Böngészőből a "Can you see me?" szöveg nem jelenhet meg, de a csomagban levő "index.php" viszont betölt. Parancssorból futtatva a webPhar nem csinál semmit és lefutnak a következő sorok. A webPhar a mapPhar()-hoz hasonlóan egy aliast is tud rendelni az archívumhoz, bár a példa ezt most nem használja ki.

Saját routing megvalósítása webPhar-ral

[GitHub link]

Szépséghibája a webPhar-nak, hogy látszólag nem tud mit kezdeni a query stringgel a php-be beépített szervert használva és ilyenkor letöltésre kínálja a fájlt. Könnyen lehet viszont egy mindenhol működő routingot megvalósítani.

A webPhar második paraméterében az általa betöltendő index fájl nevét várja. Ha ezt elhagyod, akkor az "index.php" lesz az alapértelmezett. Ezen kívül 3. paraméterben egy 404-es HTTP hibát kezelő fájl helyét várja, a 4. pedig a fájlok kiterjesztés alapján meghatározott tartalomtípusainak tömbje lesz. De az alapértelmezett a legtöbb esetben jó. Elhagyva, vagy üres tömböt átadva az alapértelmezett lesz érvényes. Utolsó paramétere viszont egy függvény, ami egyetlen paramétert vár, ami a PHAR által látott REQUEST_URI. Bizonyos esetekben ez a PATH_INFO szerverváltozóból jön (fast-cgi vagy cgi).

Ebben a függvényben a kapott URI alapján annak a fájlnak az útvonalát kell visszaadni, amit be kell tölteni. Így a htaccess-ben bevethető rewrite engine-hez hasonlóan tetszőleges kérésre tetszőleges fájl dolgozhatja fel a kérést. Az egyik legegyszerűbb megoldással kiküszöbölhető a beépített szerver problémája:

  1. Phar::webPhar('self.phar', 'index.php', '404.php', [], function ($uri) {
  2.     return parse_url($uri, PHP_URL_PATH);
  3. });

Ha ez a függvény nem egy útvonalat ad vissza, hanem logikai FALSE értéket, akkor a kért fájl helyett a "403 Access Denied" választ adja csak a szerver. Ez megfelel a htaccess-ben a "Deny from all" -nak. Egy htaccess-es rewrite valahogy így oldható meg legegyszerűbben webPhar-ral:

  1. Phar::webPhar('self.phar', 'welcome', '404.php', [], function ($uri) {
  2.     $path = parse_url($uri, PHP_URL_PATH);
  3.  
  4.     $routing = [
  5.         '/welcome' => 'index.php',
  6.         '/secret.txt' => false,
  7.     ];
  8.  
  9.     return isset($routing[$path]) ? $routing[$path] : $path;
  10. });

Összetettebb routing reguláris kifejezéssel

[GitHub link]

Az előző példában alkalmazott routing nem rossz, de minden fájlra egyenként meg kell adni, hogy elérhető-e, illetve milyen kért útvonal esetén kell válaszként visszaadni. Ennél egy kicsit hatékonyabb verziója, amikor a $routing asszociatív tömb indexe nem a konkrét útvonal, hanem egy reguláris kifejezés. Ezeket ciklusban lehet vizsgálni és az első illeszkedő mintánál megállni és visszaadni az adott indexen levő értéket. Ami akár a FALSE is lehet. Így egy teljes mappa levédése az alábbi módon oldható meg:

  1. Phar::webPhar('self.phar', 'welcome', '404.php', [], function ($uri) {
  2.     $path = parse_url($uri, PHP_URL_PATH);
  3.  
  4.     $routing = [
  5.         '/welcome' => 'index.php',
  6.         '/secret(/.*)?' => false,
  7.     ];
  8.  
  9.     foreach ($routing as $pattern => $destination) {
  10.         if (preg_match('#^' . $pattern . '$#', $path)) {
  11.             return $destination;
  12.         }
  13.     }
  14.  
  15.     return $path;
  16. });

Természetesen még ez is elég primitív megoldás, de látható, hogy tetszőleges logika is megvalósítható.

JS, CSS betöltése és az index fájl elérése álnévvel

[GitHub link]

Egy webalkalmazás nem nagyon képzelhető el CSS és gyakran javascript nélkül. Ezeket a statikus fájlokat is be lehet tenni a phar fájlba. Általában ezeket közvetlenül szeretnénk visszaadni a böngészőnek, nem egy tetszőleges alias-on keresztül. Tehát, az előző példa alapján ezekre a statikus fájlokra nem kell álnevet beállítani. Ettől persze a php értelmezőt nem lehet megúszni, hiszen eleve a phar fájlt is az dolgozza fel.

Mivel önmagában a CSS és JS betöltése nem kihívás, megbolondítottam a példát két dologgal is. Az első a tény, hogy a webPhar második paraméterében megadott index fájlnak léteznie kell, tehát nem adható meg álnév. Például "welcome", ami az "index.php" helyett látszik a webcímben. Ez kikerülhető azzal a trükkel, hogy leteszel a phar-ba egy kiterjesztés nélküli "welcome" nevű, akár üres fájlt. Ezzel a fájl már létezni fog, de a routing miatt úgysem az fog betölteni. Nem akad viszont meg már az indulásnál.

A másik, hogy a CSS és JS eléréséhez célszerű dinamikusan előállítani az abszolút URL-t. Azért, hogy ezt ne kelljen külön konfigurációs fájlban statikusan megadni, a routing függvényében megkapott $uri paraméter és a valós REQUEST_URI alapján kiszámítható a phar fájl webes elérése. Ehhez viszonyítva lehet hivatkozni a stílusfájlokra.

  1. Phar::webPhar('self.phar', 'welcome', '404.php', [], function ($uri) {
  2.     $path = parse_url($uri, PHP_URL_PATH);
  3.     $realUri = filter_input(INPUT_SERVER, 'REQUEST_URI');
  4.     $realPath = parse_url($realUri, PHP_URL_PATH);
  5.  
  6.     $diffPath = substr($realPath, 0, -strlen($path));
  7.  
  8.     define('BASE_URL', $diffPath);
  9.  
  10.  
  11.     $routing = [
  12.         '/welcome' => 'index.php',
  13.     ];
  14.  
  15.     foreach ($routing as $pattern => $destination) {
  16.         if (preg_match('#^' . $pattern . '$#', $path)) {
  17.             return $destination;
  18.         }
  19.     }
  20.  
  21.     return $path;
  22. });
  1. <link rel="stylesheet" href="<?= BASE_URL ?>/css/default.css" type="text/css">

Konfigurációs fájl felcsatolása

[GitHub link]

Még egy fontos eleme lehet egy webalkalmazásnak. Ez pedig a konfigurációs fájl. Ha a phar fájlt hordozhatóvá akarod tenni, nem célszerű beletenni például a config.php-t. A program viszont a saját gyökérkönyvtárában várná valószínűleg normál esetben. Ilyenkor hasznos a Phar::mount() metódus, amit a stub.php-ben használva a phar fájlon kívüli konfigurációs fájl felcsatolható egy, a phar fájlon belüli helyre. Ehhez előre meg kell határozni, hogy pontosan hol kell lennie a config.php-nek a phar-hoz képest. Esetleg szerveroldali környezeti változóból várni az útvonalat. Konstans viszont nem adható át, mivel ahhoz a phar fájlt include-dal be kellene tölteni a konstans definiálása után egy php fájlban. Akkor viszont a webPhar nem kezelné le a kéréseket.

Így néz ki tehát a megoldás:

  1. $configPath = __DIR__ . '/config.php';
  2.  
  3. if (is_file($configPath)) {
  4.     Phar::mount('config.php', $configPath);
  5. }
  6. // Phar::webPhar(...)

Elméletileg létezik mód arra is, hogy a phar fájlon kívül határozza meg a fejlesztő, hogy melyik fájlt csatolja fel az archívumba, de ez egy részt webPhar-ral együtt nem működik, más részt a jelek szerint másképp sem. Én legalábbis nem tudtam működésre bírni még a PHP értelmező C forráskódjának nézegetése után sem.

Tapasztalt problémák

Néhány problémát írtam a példákkal kapcsolatban. De nézzük most sorban, kiemelve ezeket.

  • A Phar fájlok kiterjesztése értelemszerűen a "phar". De már a built-in szerver sem képes kezelni böngészőből megnyitva. Más szervereknél is előforduhat, hogy ehhez külön konfiguráció szükséges, bár egy osztálykönyvtár esetén ez nem okoz gondot, ami csak include-olva van.
  • Egyes célokra több módszer is van, de ezek nem egységesek. Lásd a running() metódust, ami csak a stub-ban nem működik, a __FILE__ konstanst, ami csak a stub-ban adja vissza a phar fájlt, valamint a mapPhar-ral beállított álnevet, ami helyett talán jobb egy általánosabb, phar-tól független megoldás a gyökérkönyvtárra hivatkozáshoz, ami gyakorlatilag a phar fájl maga.
  • Megint csak a built-in szerver nem kezeli jól a query string-eket, ami miatt mindenképp egyedi routing-ot kell megvalósítani. Még ha ez rettentően egyszerű is..
  • Egyes szervereken előfordulhat az alapértelmezett index fájl betöltésekor végtelen átirányítás. Ez persze megfelelő szerverkonfigurációval elkerülhető, mégis egy fokkal megnehezíti a platformfüggetlenséget, mivel számítani kell a formátumra előre.
  • A Phar::mungServer() metódust a cikkben meg sem említettem, mivel a PHP értelmező C forrásának több napos tanulmányozása után sem jöttem rá, mit kellene egészen pontosan csinálnia. A dokumentáció pedig annyit említ, hogy eltünteti a phar nyomait. Semmilyen módon nem sikerült viszont nyomokat előidéznem, hogy legyen mit eltüntetnie. Nagyobb gond viszont, hogy sem a dokumentáció, sem más forrásban utalást sem találtam arra, hogy mi a pontos hatása a kódra. Ellenben hibajelentésben találtam rá hivatkozást. Válasz viszont ott sem volt. Ha valami ennyire dokumentálatlan és bizonytalan a működése, és nem is beszél róla senki, az elég ok arra, hogy kételkedve forduljunk a formátum felé
  • A Phar::mount() -nak a dokumentáció szerint működnie kellene úgy is, hogy nem a phar fájlban használom, hanem egy tetszőleges fájlban, ahol utána include-dal betöltöm a phar fájlt. Ám ezt a dokumentációban található eredeti példával sem sikerült működésre bírni. Szintén a PHP értelmező forráskódjához fordultam, de nem leltem meg a választ.
  • Ha feltesszük, hogy valahogyan működik a mount a stub fájlon kívül is, sajnos nem használható együtt a webPhar-ral, ami az egyik leghasznosabb dolog a formátumnál.
  • Nem is annyira probléma, inkább egy oka annak, amiért a Java-ban gyakori jar fájlokba csomagolt osztálykönyvtárak szerepét nem tölti be a phar. Van egy megszokott, elterjedt megoldás a függőségek kezelésére. Ez pedig a Composer. Jelen pillanatban viszont nem tudok róla, hogy a phar fájlokat natívan kezelné bármilyen módon. Vannak ugyan szkriptek, amik ezen próbálnak segíteni, de azért ez még nem változtatja meg senki véleményét arról, hogy kell-e ez neki.

Összefoglalás

A cikk elkezdésekor az volt a célom, hogy bebizonyítsam, milyen hasznos eszközről van szó. Sajnos rá kellett jönnöm, hogy élesben egyedül az olyan parancssori alkalmazásoknál nem tudok belekötni, ahol nincs szükség külső konfiruációs fájlra. Egyetlen fájlban minden, egyszerűen használható. A gondok azoknál a metódusoknál kezdődnek, amik a webes futtatásnál próbálnak útvonalakat helyreállítani, automatikus index fájlra átirányítani vagy akkor, amikor jó lenne felcsatolni egy nem fix helyen várt konfigurációs fájlt vagy akár "uploads" mappát. Ez egy elég nagy területen nehezíti meg az alkalmazását és érthető módon nem hozza meg a fejlesztő kedvét hozzá.

Mégis, speciális esetben, amikor például nincs lehetőség egy szoftver által használt speciális fájlkiterjesztést engedélyezni a szerveren, amit a php értelmezőn át kellene engedni, a webPhar 4. paraméterében megadható mime típusokkal ezen lehet segíteni. Illetve htaccess rewrite is szimulálható. Ehhez a már kész alkalmazást be kell csomagolni egy phar-ba, ami vagy sikerül, vagy nem. Ugyanis a relatív hivatkozásokkal is megyűlhet a bajunk.

Mindent összevetve egy korához képest túl kidolgozatlan, bár hasznos célokat kitűző eszközről van szó, amit látszólag a PHP értelmező fejlesztői sem tartanak elég központi szerepűnek. Egy phpunit, composer és hasonló eszközök becsomagolására és terjesztésére viszont tökéletes. És már szinte gyakorlattá vált.

Érdemes tehát tudni róla, de a cikk megírása közben arra jutottam, hogy senki ne érezze úgy, hogy valami szenzációs dolgot hagyott ki, ha még nem jutott eszébe phar fájlokba csomagolnia a munkáját. Éppen ezért egyes, kevésbé fontosnak ítélt kérdésekkel nem foglalkoztam a cikkben. Ilyen például a formátumok közötti konvertálás, illetve a zip vagy tar fájlokba tömörítés a Phar osztály nélkül, illetve ezzel kapcsolatban a PharData osztály. Aki mégis úgy érzi, enélkül hiányérzete van, még pótolhatja ismereteit utánanézéssel.

Kategóriák: 
Megosztás/Mentés

Új hozzászólás