PHP 5.3: de PHAR extensie
Het heeft even geduurd, net als met namespaces, maar uiteindelijk is ook de PHAR extensie in de core distributie van PHP 5.3 terecht gekomen. De PHAR extensie, welke standaard wordt meegecompileerd in de nieuwe PHP versie, stelt je in staat al je PHP code te bundelen tot 1 PHAR bestand en de website ook direct uit dit bestand te serveren.
Hoe werkt PHAR?
Wacht. Wat? Alle PHP bestanden bundelen tot 1 bestand? Hoe kan PHP daar in godsnaam mee omgaan? Nouja, dat is de magie van de PHAR extensie. Iets minder magisch gesproken; er zijn twee belangrijke elementen die bijdragen aan deze extensie. Ten eerste is daar de __HALT_COMPILER() instructie die sinds PHP 5.2 in PHP zit. Hiermee kun je als scripter aangeven dat de PHP parser vanaf dat punt klaar is en moet stoppen met parsen. Dit maakt het mogelijk om alles in 1 bestand te hebben.
Ten tweede is daar de phar:// stream wrapper. Deze implementeert de kennis van het bestandsformaat en maakt het mogelijk om andere bestanden uit het archief bestand te includen. Tenslotte haalt de PHAR extensie nog enkele trucjes uit om het gebruik van PHAR extensies nog iets makkelijker te maken.
Hoe maak ik een PHAR bestand?
Met __HALT_COMPILER() en de phar stream wrapper heb je verder niet zo heel veel te maken. Dat is de onderliggende techniek. Met de bijgeleverde Phar klasse hoef je enkel maar de juiste methodes aan te roepen om een PHAR bestand te maken.
PHAR biedt enkele manieren om een PHAR bestand samen te stellen. Je kunt bijvoorbeeld een hele directory opnemen in een PHAR bestand door PHAR::buildFromDirectory() te gebruiken:
-
$phar = new Phar('mijn-project.phar');
-
$phar->buildFromDirectory('/pad/naar/mijn-project');
-
$phar->setStub($phar->createDefaultStub('index.php'));
Deze code maakt een nieuw PHAR bestand mijn-project.phar aan met daarin de inhoud van /pad/naar/mijn-project. Om de code ook daadwerkelijk op te starten hebben we een zogenaamde stub nodig, een soort opstart bestand. Met PHAR::createDefaultStub() heb je de meest functionele stub, maar, zoals we later zullen zien, is deze stub ook wat langzaam.
Een andere manier is om met een setje iterators en PHAR::buildFromIterator() precies op te geven welke bestanden en eventuele directories waar ze in staan op te nemen in het PHAR archief:
-
class FileExtensionFilterIterator extends FilterIterator {
-
-
public function __construct($allowedExtensions, $it) {
-
$this->_allowedExtensions = $allowedExtensions;
-
parent::__construct($it);
-
}
-
-
public function accept() {
-
-
}
-
}
-
-
/* We willen alleen alle PHP bestanden hebben: */
-
$iterator = new FileExtensionFilterIterator (
-
new RecursiveIteratorIterator (
-
new RecursiveDirectoryIterator ( 'mijn-applicatie/' )
-
)
-
);
-
-
$phar = new Phar('mijn-applicatie.phar');
-
$phar->buildFromIterator($iterator);
-
$phar->setStub($phar->createDefaultStub('index.php'));
Naar mijn bescheiden mening is het het makkelijkst om PHAR bestanden op de commandline te genereren. Geen gedoe met permissies, memory limits of execution time. Uit veiligheidsoverwegingen staat PHAR je niet zonder meer toe om PHAR bestanden weg te schrijven. Daarvoor zul je de configuratie optie phar.readonly voor moeten wijzigen. Dat kunnen we doen met de '-d' commandline optie voor PHP:
-
$ php -d phar.readonly=0 build-phar.php
Het resulterende .phar bestand plaats je in je webroot en de webapplicatie kan vervolgens benaderd worden door het PHAR bestand te bezoeken via je browser. Vergeet niet om .phar toe te voegen aan de extensies die door de PHP parser worden opgepakt:
-
AddType application/x-httpd-php .php .phtml .phar
Of, als dat geen optie is, kun je het PHAR bestand ook gewoon hernoemen naar .php.
Performance
Een niet onbelangrijk aspect aan PHAR is natuurlijk de performance. In een vroeger stadium was PHAR een stuk trager dan een gewoon script, maar tegenwoordig is PHAR in staat om vanuit een PHAR bestand vrijwel dezelfde prestaties te leveren als dan dat de bestanden gewoon vanaf het filesystem worden geserveerd. Daar moet je echter wel iets voor doen.
PHAR bestand cachen
Ten eerste is het mogelijk om je PHAR bestand te laten cachen op het moment dat de webserver start. Dit doe je met de configuratie optie phar.cache_list, welke je overigens alleen van httpd.conf of php.ini kunt configureren:
-
php_value phar.cache_list "/pad/naar/mijnphar.phar:/pad/naar/anderphar.phar"
Dit is uiteraard niet nuttig voor development, want, zoals ik al zei, het PHAR bestand wordt gecached op het moment dat de server start en voor wijzigingen zul je de webserver dan moeten herstarten. Maar, deze optie is wel cruciaal om acceptabele performance te behalen.
Een eigen stub creeëren
Uit mijn eerste tests bleek dat PHAR eigenlijk altijd 1/3e trager was dan gewone bestanden. Maar dat zou niet moeten, want de auteurs van PHAR claimde een verwaarloosbaar verschil tussen een webapplicatie van uit een PHAR archief en een webapplicatie direct op het filesystem. Een mailtje naar een van de auteurs bracht duidelijkheid: probeer eens een eigen kortere stub dan die standaard wordt mee geleverd en dat hielp!
De standaard stub bevat PHP code om het PHAR bestand ook op PHP 5.2 installaties te kunnen draaien. Aangezien PHP 5.2 nog niet de PHAR extensie bevat, moet deze PHP code de nodige trucs uit halen om het PHAR bestand te kunnen uitvoeren. Dat blijkt, de stub is zo'n 7Kb aan PHP code.
Op het moment dat je deze standaard stub weglaat en alleen gebruikt wat je voor PHP 5.3 nodig hebt - komt - op een manier die mij nog niet helemaal duidelijk is, de performance vrijwel gelijk te liggen met dat van het serveren vanaf het filesystem:
-
$phar = new Phar('mijn-project.phar');
-
$phar->buildFromDirectory('mijn-project/');
-
$phar->setStub("<?php
-
Phar::interceptFileFuncs();
-
set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path());
-
Phar::webPhar(null, \$web);
-
include 'phar://' . __FILE__ . '/' . Extract_Phar::START;
-
return;
-
__HALT_COMPILER(); ?>");
Omgaan met externe directories en bestanden
Er zijn bepaalde bestanden die je niet in je PHAR bestand wil hebben. Denk bijvoorbeeld aan een configuratie bestand. Je wilt niet voor iedere config update je PHAR bestand updaten. Maar ook bestanden die de gebruiker kan uploaden moeten uiteraard niet in het PHAR bestand terecht komen. Tegelijkertijd wil je niet (teveel) PHAR specifieke code toevoegen aan je project. Om deze problemen op te lossen kun je in je stub een directory of een bestand mounten. Dit maakt in feite een brug naar bestanden buiten het PHAR archief.
Om een configuratie bestand toe te voegen van buiten het PHAR bestand voeg je bijvoorbeeld het volgende toe aan je stub code:
-
Phar::mount('config.intern.php', 'config.extern.php');
Oftewel:
-
$phar = new Phar('mijn-project.phar');
-
$phar->buildFromDirectory('mijn-project/');
-
$phar->setStub("<?php
-
Phar::interceptFileFuncs();
-
set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path());
-
Phar::mount('config.intern.php', 'config.extern.php');
-
Phar::webPhar(null, \$web);
-
include 'phar://' . __FILE__ . '/' . Extract_Phar::START;
-
return;
-
__HALT_COMPILER(); ?>");
Op het moment dat een PHP bestand in het PHAR archief het bestand config.intern.php probeert te benaderen, zal deze doorgesluisd worden naar het config.extern.php dat naast het PHAR archief staat.
Conclusie
Dit artikel is slechts een korte introductie tot PHAR en beschrijft niet alle mogelijkheden, zoals het feit dat de PHAR extensie ook ZIP en TAR bestanden kan gebruiken, of dat de PHAR API meer mogelijkheden biedt om PHAR bestanden te manipuleren dan hier beschreven. Het loont dus om de handleiding van PHAR nog eens goed door te nemen als je hier mee aan slag gaat.
Het nut van PHAR zal, denk ik dan, zich voornamelijk tonen bij verschillende kleinere webapplicaties die met het gebruik van PHAR enigzins gemakkelijker te beheren zijn. Daarbuiten weet ik niet zo zeker of PHAR echt een groot publiek zal aanspreken.
Een van redenen waarom is ik dat denk is dat om z'n volledige potentie te bereiken moet het ontwikkelaars beperken, het PHAR bestand wordt gecached door de webserver en er moet een server restart aan te pas komen om de cache te reloaden. Dat is nou net iets waar we als PHP ontwikkelaars gelukkig geen last van hadden. Daarbij zullen virtual hosters niet erg happig zijn om een dergelijke opzet en dat beperkt het eventuele gebruik aanzienlijk.
Wat denk jij? Zal het gemak dat het beheren van PHAR bestanden biedt het ongemak het hoofd bieden en van PHAR een populaire nieuwe extensie maken? Of, zie jij misschien voordelen aan PHAR die ik over het hoofd zie? Laat wat horen in de comments!
Volg Scriptorama via RSS!
Reageer ook!
Eerlijk gezegd zie ik het nut niet zo. Wat maakt het voor je beheer uit of je 1 of 100 files via FTP upload? En als dat wel uitmaakt gooi je er een tar.gz of een zip op en pak je die op de commandline even uit. Bij een multi-server opstelling kan je heel eenvoudig met bijvoorbeeld Rsync de boel verspreiden vanaf een masterserver, dus dan is het ook geen argument.
Dan de performance, die gaat gewoon keihard achteruit. Als ik het allemaal goed begrijp wordt is het gewoon een soort zip-file waar je uit gaat zitten lezen, wat dus behoorlijk wat CPU power kost. Het enige voordeel is dat je random I/O afneemt, omdat je nu maar 1 file hoeft in te lezen, maar het verminderen van random I/O is over het algemeen al vrij eenvoudig op te lossen door meer geheugen in de server te zetten. Linux heeft bijvoorbeeld een uitstekende FS cache die prima werkt.
Voor beveiliging van je source hoef je het ook niet te doen. Er zit namelijk geen beveiliging in en dus valt dat ook al af.
Welke voordelen zijn er dan nog? Wat mij betreft geen of een paar hele kleintjes. Voor zover ik het nu kan zien is de PHAR extensie gewoon zonde van de moeite geweest.
Door Alex Kamsteeg
op 08.26.08 @ 9:47 am | Permalink
Het kan handig zijn voor kleine command-line applicaties of achtergrond processen en wat PEAR doet een simpele installer, maar voor website zou ik het liever niet gebruiken.
phar is eigenlijk net zo iets als jar (java archief).
Door Sebastiaan Stok
op 08.26.08 @ 10:24 am | Permalink
@alex: Je leest toch over iets heen. De performance gaat, mits je de nodige maatregelen neemt, niet 'hard achteruit'.
Door Mathieu Kooiman
op 08.26.08 @ 12:16 pm | Permalink
Dat had ik heus wel gezien, maar daarvoor moet ik wel cachen waardoor ik mijn server moet herstarten bij elke wijziging. Met wat geheugen erbij kan het hele filecaching gebeuren door het OS afgehandeld worden, waardoor ik mijn hele deployment procedure niet hoef te wijzigen.
Door Alex Kamsteeg
op 08.26.08 @ 2:57 pm | Permalink
Is een phar niet potentieel onveiliger? Wie de eigen app in een phar verpakt, moet de phar in een publieke directory zetten. Normaal gesproken zet je sommige delen (configuratie, bibliotheken) etc buiten de webmappen.
Door Alfa
op 08.26.08 @ 9:39 pm | Permalink
@alex: Precies. Het nut van PHAR is inderdaad niet helemaal duidelijk.
@Alfa: Nee, als je phar::mount() zo gebruikt dat je config files buiten het PHAR bestand wordt geplaatst, kun je er ook voor zorgen dat deze niet in de webroot staan.
Door Mathieu Kooiman
op 08.27.08 @ 7:15 am | Permalink
@Mathieu Kooiman
ja, maar van een gemiddeld zend frameworkproject bijvoorbeeld is 95% van de bestanden buiten een webmap. Alleen je index.php + css + js is publiekelijk benaderbaar. Het is wel erg bewerkelijk om dat allemaal te mounten.
Door Alfa
op 08.27.08 @ 10:10 am | Permalink
@Alfa: Het is geloof ik ook nog mogelijk om externe toegang tot bepaalde 'directories' binnen het PHAR archief te weigeren.
Door Mathieu Kooiman
op 08.27.08 @ 11:04 am | Permalink
Dat moet haast wel mogelijk zijn, lijkt me 1 van de belangrijkste features. Lees: als daar niet over nagedacht is mag het direct een pijnlijke dood sterven.
Door Maarten
op 08.27.08 @ 11:12 am | Permalink
Heb de documentatie gelezen, maar nog niets gezien wat daarop wijst. Kan zijn dat dat nog niet gedocumenteerd is natuurlijk…
Door Alfa
op 08.27.08 @ 7:09 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>