1. Einleitung
"Phar" steht für "PHP-Archiv", und Phar-Files sind dafür gedacht, komplette Applikationen, die in PHP erstellt wurden, in einer Datei ausliefern zu können, was die Perfomance erhöht, da Dateizugriffe ja "teuer" sind und bei der Verwendung eines Phar-Files nur eine Datei geöffnet werden muss, um auf die verschiedenen Inhalte zugreifen zu können. Darüber hinaus besteht auch die Möglichkeit, dass das Archiv in komprimierter Form vorliegt, wodurch Speicherplatz gespart wird. Weitere Informationen dazu finden sich u. a. in der offiziellen Phar-Dokumentation unter: https://www.php.net/manual/de/book.phar.php
Phar-Files eignen sich jedoch zu mehr als "nur" das Zusammenpacken von (Web-)Applikationen. Z. B. kann man sie dazu verwenden, eine PHP-Klassenbibliotheken in einer Datei zusammenzufassen und das Ganze so zu gestalten, dass diese Bibliothek sehr einfach benutzt werden kann.
Leider ist die Dokumentation zu Phar – wenn man einmal von den "Standardfällen" absieht – nicht sehr ausgeprägt, und selbst die offizielle Doku weist Lücken auf. Für den Fall "Phar als PHP-Klassenbibliothek" habe ich in den Weiten des Internets gar nichts gefunden, weshalb es nun dieses Dokument gibt.
Ziel dieses Artikels ist es, zunächst eine rasche Einführung ins Thema "Phar" zu bieten (rasch deshalb, weil es dazu ja, wie gesagt, einiges gibt) und dann im weiteren Verlauf darzulegen, wie man Phar speziell für die Zurverfügungstellung von PHP-Klassenbibliotheken nutzen kann.
[Zurück zum Inhaltsverzeichnis]
2. Vorbereitung
Bevor wir Phar-Files erstellen können, müssen wir sicherstellen, dass PHP das überhaupt zulässt. Die Standard-Einstellung ist nämlich, dass es nicht geht, was in Produktivumgebungen auch äußert ratsam ist. Um das Erstellen zu erlauben, müssen wir in der php.ini die Einstellung phar.readonly suchen und diese auf 0 oder false setzen, also:
phar.readonly = 0
Das war's schon.
[Zurück zum Inhaltsverzeichnis]
3. Ein einfaches Beispiel
An einem einfachen Beispiel soll kurz demonstriert werden, wie Phar-Files grundsätzlich erstellt und verwendet werden. Als Erstes benötigen wir ein Verzeichnis mit den Dateien, die den Inhalt des fertigen Phar-Files bilden sollen, z. B.:
src/ |-- content.md `-- index.php
Der Inhalt der Datei src/content.md:
# Content-Test für Phar Dies ist der Content.
Der Inhalt der Datei src/index.php:
<?php echo "phar application started\n";
Um daraus ein Phar-File zu erstellen, wird ein kleines Script benötigt. Wir nennen es create-phar.php und geben ihm folgenden Inhalt:
<?php // filename $pharFile = 'app.phar'; // clean up @unlink( $pharFile ); // create phar $p = new Phar( $pharFile ); // use whole directory $p->buildFromDirectory( 'src/' ); // point to main file $p->setDefaultStub( 'index.php', '/index.php' ); echo "$pharFile created\n";
Das Script wird mit
php create-phar.php
aufgerufen.
Das fertige Phar-File kann jetzt mittels
php app.phar
direkt gestartet werden. Wir sehen, dass dabei die Datei src/index.php ausgeführt wird:
$ php app.phar phar application started
Neben dem Starten der im Phar-File gespeicherten Applikation kann auch gezielt auf einzelne Dateien im Phar-File zugegriffen werden. Um das zu demonstrieren, erstellen wir eine weiteres Script, geben ihm den Namen use-wrapper.php und folgenden Inhalt:
<?php echo file_get_contents( 'phar://app.phar/content.md' );
Das Aufrufen des Scripts erzeugt folgende Ausgabe:
$ php use-wrapper.php # Content-Test für Phar Dies ist der Content.
[Zurück zum Inhaltsverzeichnis]
4. Klassenbibliothek mit Autoloader
Die Strukur des Quellverzeichnisses sieht diesmal so aus:
src/ |-- class1.php |-- class2.php |-- index.php `-- ns1 `-- class1.php
Der Inhalt von src/class1.php:
<?php class class1 { public function write() { echo "Ich bin Klasse 1.\n"; } }
Der Inhalt von src/class2.php:
<?php class class2 { public function write() { echo "Ich bin Klasse 2.\n"; } }
Der Inhalt von src/index.php:
<?php echo "Dieses Phar-File enthält lediglich eine Klassensammlung und ist nicht dafür gedacht, direkt ausgeführt zu werden.\n";
Der Inhalt von src/ns1/class1.php:
<?php namespace ns1; class class1 { public function write() { echo "Ich bin Klasse 1 aus Namespace 1.\n"; } }
Das Script create-phar.php zum Erzeugen des Phar-Files classes.phar:
<?php // filename $pharFile = 'classes.phar'; // clean up @unlink( $pharFile ); // create phar $p = new Phar( $pharFile ); // use whole directory $p->buildFromDirectory( 'src/' ); // point to main file $p->setDefaultStub( 'index.php', '/index.php' ); echo "$pharFile created\n";
Damit wäre es bereits möglich, die Klassen aus dem Phar-File zu verwenden, indem der Phar-Wrapper verwendet wird, z. B.:
<?php require_once 'phar://classes.phar/class1.php'; $c1 = new class1(); // etc.
Komfortabler wird es via Autoloading, speziell dann, wenn mehrere Klassen aus dem Phar-File verwendet werden sollen. Die Datei für den Autoloader nennen wir autoloader.php und geben ihr folgenden Inhalt:
<?php spl_autoload_register( function( $class ) { $dir = str_replace( '\\', '/', __DIR__ ); $loadName = 'phar://'.$dir.'/classes.phar/'.str_replace( '\\', '/', ltrim( $class, '\\' ) ).'.php'; if ( file_exists( $loadName ) ) { include $loadName; } });
Um das Ganze zu testen, erstellen wir das Script use-autoloader.php mit folgendem Inhalt:
<?php require_once 'autoloader.php'; $c1 = new class1(); $c1->write(); $c2 = new class2(); $c2->write(); $ns1c1 = new \ns1\class1(); $ns1c1->write();
Das Ausführen des Scripts erzeugt folgende Ausgabe:
$ php use-autoloader.php Ich bin Klasse 1. Ich bin Klasse 2. Ich bin Klasse 1 aus Namespace 1.
[Zurück zum Inhaltsverzeichnis]
5. Klassenbibliothek mit Autoloader-Stub
Die im Abschnitt "Klassenbibliothek mit Autoloader" beschriebene Vorgehensweise ist schon recht chic, aber es geht noch besser. Wir machen uns dabei den Umstand zunutze, dass jedes Phar-File über ein sog. Stub-Modul verfügt, das ausgeführt wird, wenn man das Phar-File als solches "aufruft".
Das Standard-Stub-Modul ist dabei so ausgelegt, dass das Phar-File als Applikation genutzt wird. Wir wollen aber etwas anderes, nämlich das Phar-File als Klassenbibliothek nutzen und zwar möglichst bequem, performant und platzsparend. Wie das geht, sehen wir in diesem Kapitel.
Der Inhalt des Verzeichnisses src/ ist derselbe wie im Abschnitt "Klassenbibliothek mit Autoloader". Die Dateien autoloader.php und classes.phar werden nicht mehr benötigt und können (sollten) gelöscht werden. Das Script create-phar.php bekommt einen anderen Inhalt:
<?php // filenames $pharFile = 'libs.phar'; $pharGzFile = $pharFile.'.gz'; // stub module $stub = <<<'EOT' <?php spl_autoload_register( function( $class ) { $file = str_replace( '\\', '/', __FILE__ ); $loadName = 'phar://'.$file.'/'.str_replace( '\\', '/', ltrim( $class, '\\' ) ).'.php'; if ( file_exists( $loadName ) ) { include $loadName; } }); __HALT_COMPILER(); ?> EOT; // clean up @unlink( $pharGzFile ); // create phar $p = new Phar( $pharFile ); // set compression $p = $p->compress( Phar::GZ ); // set stub-code $p->setStub( $stub ); // create the LIB $p->buildFromDirectory( 'src/' ); echo "created {$pharGzFile}\n";
Dieses Script enthält folgende "Besonderheiten" (besser: "Erweiterungen") gegenüber seinem Vorgänger:
- Aus dem Dateinamen des Phar-Files wird eine hergeleitete Version erzeugt, die
den Dateinamen des komprimierten Phar-Files enthält.
Hintergrund: Für die gewählte Komprimierung Phar::GZ wird von der Phar-Klasse immer .gz an den Dateinamen angehängt. - Es wird eine Variable $stub angelegt, die den Code
des Autoloaders enthält, jedoch genau hinschauen: Der Autoloader wurde gegenüber dem vorigen
Kapitel ebenfalls geändert!.
Am Ende steht die Anweisung __HALT_COMPILER();, die der Phar-Klasse das Ende eines Stub-Moduls signalisiert. - Beim eigentlichen Erstellen des Phar-Files wird zuerst die Kompression eingestellt und dann der Inhalt der Variablen $stub als Stub-Modul für dieses Phar-File gesetzt.
Das so erzeugte Phar-File ist sehr kompakt und ermöglicht das bequeme und direkte Verwenden der enthaltenen Klassenbibliothek, wie die geänderte Fassung von use-autoloader.php zeigt:
<?php require_once 'libs.phar.gz'; $c1 = new class1(); $c1->write(); $c2 = new class2(); $c2->write(); $ns1c1 = new \ns1\class1(); $ns1c1->write();
Es wird also kein separater Autoloader mehr benötigt, das Phar-File muss nur einmal eingebunden werden, damit die darin enthaltene Klassenbibliothek verwendet werden kann. Der Aufruf des Test-Scripts erzeugt wie erwartet folgende Ausgabe:
$ php use-autoloader.php Ich bin Klasse 1. Ich bin Klasse 2. Ich bin Klasse 1 aus Namespace 1.
Der dergestalt in das Phar-File eingebundene Autoloader hat zudem den Vorteil, dass er sich sozusagen "automatisch" an den Namen des Phar-Files anpasst. Beim Autoloader aus dem Abschnitt "Klassenbibliothek mit Autoloader" musste in Zeile 4 noch der Name des Phar-Files angegeben und bei Änderungen desselben jedes Mal entsprechend angepasst werden. In der Fassung aus diesem Kapitel genügt es, den Namen des Phar-Files in Zeile 2 des Scripts create-phar.php zu setzen, sodass dieses Script leicht auch für andere Klassenbibliotheken wiederverwendet werden kann.
[Zurück zum Inhaltsverzeichnis]
6. Klassenbibliothek mit Autoloader-Stub und Metadaten
Eine Klassenbibliothek ist erfahrungsgemäß nicht statisch, sondern wird von Zeit zu Zeit weiterentwickelt. Spätestens dann kommt die Frage nach Versionierung auf und vor allem auch danach, wie man der Bibliothek leicht "ansehen" kann, welche Version man denn da gerade vor sich hat und in einem bestimmten Projekt verwendet.
U. a. für diesen Zweck bietet es sich an, die Möglichkeit von Phar-Files, Metadaten hinterlegen zu können, zu nutzen. Das wollen wir uns an einem Beispiel anschauen. Struktur und Inhalt des Verzeichnisses src/ bleiben wie im Abschnitt "Klassenbibliothek mit Autoloader-Stub", das Script create-phar.php wird jedoch ein wenig erweitert:
<?php // meta data $metaData = array( 'name' => 'myLib' , 'copyright' => '(c) 2021 by myself' , 'description' => 'my fabulous class library' , 'created' => date( 'Y-m-d H:i:s (e)' ) ); // filenames $pharFile = $metaData['name'].'.phar'; $pharGzFile = $pharFile.'.gz'; // stub module $stub = <<<'EOT' <?php spl_autoload_register( function( $class ) { $file = str_replace( '\\', '/', __FILE__ ); $loadName = 'phar://'.$file.'/'.str_replace( '\\', '/', ltrim( $class, '\\' ) ).'.php'; if ( file_exists( $loadName ) ) { include $loadName; } }); __HALT_COMPILER(); ?> EOT; // clean up @unlink( $pharGzFile ); // create phar $p = new Phar( $pharFile ); // set compression $p = $p->compress( Phar::GZ ); // set stub-code $p->setStub( $stub ); // set meta data $p->setMetadata( $metaData ); // create the LIB $p->buildFromDirectory( 'src/' ); echo "created {$pharGzFile}\n";
Die Ergänzungen bzw. Änderungen im Einzelnen:
- Zu Beginn des Scripts werden die Metadaten definiert. Das erfolgt immer in Form eines assoziativen Arrays. Der Inhalt ist dabei im Prinzip wahlfrei, allerdings sind die Metadaten nicht dafür gedacht, größere Datenmengen aufzunehmen; das würde u. a. das Laden des Phar-Files verlangsamen.
- Der Basisname für das fertige Phar-File wird aus dem Eintrag name der Metadaten gelesen; daraus werden die beiden Dateinamen generiert. Das hat den Vorteil, dass bei Verwendung des Scripts für andere Projekte nur die Metadaten geändert werden müssen (was sie für ein neues Projekt ohnehin werden), der Rest passt sich sozusagen "automagisch" an.
- Beim eigentlichen Erstellen des Phar-Files werden die Metadaten dann mittels $p->setMetadata( $metaData ); eingetragen.
Mit einem kleinen Script können die Metadaten jetzt aus diesem Phar-File ausgelesen werden, um die gewünschten Informationen zu liefern. Wir nennen es read-meta.php und geben ihm folgenden Inhalt:
<?php // open phar $p = new Phar( 'myLib.phar.gz' ); // retrieve meta data $metaData = $p->getMetadata(); // dump meta data print_r( $metaData );
Dieses Verfahren hat allerdings folgende Nachteile:
- Der Dateiname des Phar-Files ist fest codiert und müsste für jedes neue Phar-File angepasst werden. Alternativ könnte man sich überlegen, dem Script den Dateinamen als Aufrufparameter mitzugeben, was das Script aber auch gleich deutlich aufwendiger macht, weil eine entsprechende Parameterprüfung und -auswertung implementiert werden muss.
- Man muss zusammen mit dem Phar-File auch immer das Script zum Auslesen der Metadaten ausliefern.
Durch ein Änderung des Stub-Moduls lassen sich diese Nachteile jedoch beseitigen. Das Script create-phar.php sieht dann so aus:
<?php // meta data $metaData = array( 'name' => 'myLib' , 'copyright' => '(c) 2021 by myself' , 'description' => 'my fabulous class library' , 'created' => date( 'Y-m-d H:i:s (e)' ) ); // filenames $pharFile = $metaData['name'].'.phar'; $pharGzFile = $pharFile.'.gz'; // stub module $stub = <<<'EOT' <?php if( isset( $GLOBALS['argc'] ) && $GLOBALS['argc'] > 1 && $GLOBALS['argv'][1] == '-V' ) { $p = new Phar( __FILE__ ); $metaData = $p->getMetadata(); echo $metaData['name'].' Version '.$metaData['created'].' - '.$metaData['copyright']."\n"; } else { spl_autoload_register( function( $class ) { $file = str_replace( '\\', '/', __FILE__ ); $loadName = 'phar://'.$file.'/'.str_replace( '\\', '/', ltrim( $class, '\\' ) ).'.php'; if ( file_exists( $loadName ) ) { include $loadName; } }); } __HALT_COMPILER(); ?> EOT; // clean up @unlink( $pharGzFile ); // create phar $p = new Phar( $pharFile ); // set compression $p = $p->compress( Phar::GZ ); // set stub-code $p->setStub( $stub ); // set meta data $p->setMetadata( $metaData ); // create the LIB $p->buildFromDirectory( 'src/' ); echo "created {$pharGzFile}\n";
Das Script read-meta.php wird dann nicht mehr benötigt. Die Metadaten des Phar-Files lassen sich mittels
php myLib.phar.gz -V
anzeigen. In der vorgestellten Version sind das zwar "nur" Basisname, Zeitstempel und Copyright, diese zeigen jedoch bereits als Einzeiler die wesentlichen Information zum Phar-File an. Entsprechende Anpassungen an die eigenen Bedürfnisse sollten sich leicht vornehmen lassen, indem der Stub-Code entsprechend erweitert wird.
Wichtig: Wird der Parameter -V angegeben, wird der im Stub-Modul enthaltene Autoloader nicht registriert! Die Idee dahinter ist, dass bei Ausgabe der Metadaten keine "normale" Verwendung der Klassenbibliothek stattfindet, sondern eben "nur" die LIB-Infos angezeigt werden sollen. Wird das Phar-File wie gewohnt mittels
<?php require_once 'libs.phar.gz';
eingebunden, werden die Metadaten nicht ausgegeben, stattdessen wird der Autoloader registriert, sodass die Klassen der Bibliothek wie gewohnt verwendet werden können.
[Zurück zum Inhaltsverzeichnis]
7. Noch kleiner und schneller
Wie wir gesehen haben, lässt sich ein Phar-File mittels buildFromDirectory sehr komfortabel aus dem Inhalt eines Verzeichnisses erstellen. Die Zeile
$p = $p->compress( Phar::GZ );
sorgt dabei dafür, dass das resultierende Phar-File sich von der Größe her einigermaßen im Rahmen hält. Allerdings kann man noch mehr tun, um die resultierende Größe zu reduzieren, und als Bonus wird das Phar-File dadurch sogar noch schneller geladen.
Gut wartbarer Code zeichnet sich nicht zuletzt dadurch aus, dass er gut kommentiert ist. Allerdings kosten diese Kommentare auch Platz. Trotzdem ist es keine gute Idee, deshalb auf das Kommentieren zu verzichten. Stattdessen hat PHP eine Funktion im Werkzeugkasten, die uns an dieser Stelle weiterhilft: php_strip_whitespace
Wie der offiziellen Dokumentation zu entnehmen ist, lädt die Funktion eine PHP-Datei, entfernt alle nicht relevanten Whitepsaces sowie alle Kommentare und gibt das Ergebnis als String zurück. Genau das brauchen wir! Zwar wird das Script zum Erzeugen des Phar-Files dadurch ein wenig aufwendiger, aber diese Arbeit muss man sich ja nur einmal machen und erhält im Gegenzug die o. g. Vorteile. Darüber hinaus bleibt der eigentliche Quellcode gut kommentiert.
Betrachten wir zunächst den Code-Abschnitt, der die Zeile
$p->buildFromDirectory( 'src/' );
ersetzt:
foreach ( $dirEntries as $entry ) { if ( $entry->isFile() ) { $file = $entry->getPathname(); echo " -> {$file}\n"; if ( preg_match( '/\\.php$/i', $file ) ) { $p->addFromString( substr( $file, $srcOffset ), php_strip_whitespace( $file ) ); } else { $p->addFile( $file, substr( $file, $srcOffset ) ); } } }
Die Schleife iteriert über alle Einträge des Quellverzeichnisses (dazu gleich mehr) und verarbeitet dabei nur Dateien. Bei denen wiederum wird geprüft, ob sie die Endung .php besitzen, und falls ja, werden sie unter Verwendung von php_strip_whitespace aufs Wesentliche reduziert und dann dem Phar-File hinzugefügt. Alle anderen Dateien werden mittels addFile ohne Veränderung ins Phar-File aufgenommen.
An sich sorgt das schon für eine gute Komprimierung des Phar-Files, man kann sich also überlegen, ob man es zusäzlich noch zippen möchte oder nicht (vgl. die vorigen Kapitel). Vor- und Nachteil liegen auf der Hand: Gezippt ist das Phar-File noch kleiner, ungezippt wird es etwas schneller geladen, weil das Entpacken entfällt.
Interessant ist noch das Code-Snippet
substr( $file, $srcOffset )
welches in beiden Fällen dafür sorgt, dass das Basisverzeichnis aus dem Phar-internen Pfad der jeweiligen Datei entfernt wird. Diese Arbeit hatte uns bislang die Funktion buildFromDirectory abgenommen.
Bleibt noch die Frage zu klären, welche Vorbereitungen getroffen werden müssen, damit die Schleife über den Inhalt des Quellverzeichnisses iterieren kann:
$srcOffset = strlen( rtrim( $srcDir, '/' ) ) + 1; $dirEntries = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $srcDir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST );
Die erste Zeile bestimmt dabei die Anzahl der Zeichen, die übersprungen werden müssen, um das Basisverzeichnis aus dem Pfad zu entfernen, die zweite Zeile bereitet den eigentlichen Iterator vor.
Das fertige Script zum Erstellen des Phar-Files kann dann z. B. so aussehen:
<?php // meta data $metaData = array( 'name' => 'myLib' , 'copyright' => '(c) 2021 by myself' , 'description' => 'my fabulous class library' , 'created' => date( 'Y-m-d H:i:s (e)' ) ); // directory- and filenames $srcDir = 'src/'; $pharFile = $metaData['name'].'.phar'; $pharGzFile = $pharFile.'.gz'; // stub module $stub = <<<'EOT' <?php if( isset( $GLOBALS['argc'] ) && $GLOBALS['argc'] > 1 && $GLOBALS['argv'][1] == '-V' ) { $p = new Phar( __FILE__ ); $metaData = $p->getMetadata(); echo $metaData['name'].' Version '.$metaData['created'].' - '.$metaData['copyright']."\n"; } else { spl_autoload_register( function( $class ) { $file = str_replace( '\\', '/', __FILE__ ); $loadName = 'phar://'.$file.'/'.str_replace( '\\', '/', ltrim( $class, '\\' ) ).'.php'; if ( file_exists( $loadName ) ) { include $loadName; } }); } __HALT_COMPILER(); ?> EOT; // additional vars $srcOffset = strlen( rtrim( $srcDir, '/' ) ) + 1; $dirEntries = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $srcDir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); // clean up @unlink( $pharGzFile ); // create phar $p = new Phar( $pharFile ); // set compression $p = $p->compress( Phar::GZ ); // set stub-code $p->setStub( $stub ); // set meta data $p->setMetadata( $metaData ); // create the LIB echo "creating {$pharGzFile}\n"; foreach ( $dirEntries as $entry ) { if ( $entry->isFile() ) { $file = $entry->getPathname(); echo " -> {$file}\n"; if ( preg_match( '/\\.php$/i', $file ) ) { $p->addFromString( substr( $file, $srcOffset ), php_strip_whitespace( $file ) ); } else { $p->addFile( $file, substr( $file, $srcOffset ) ); } } } // Fertig. echo "done.\n";