Arbeiten mit Phar-Archiven
Ich nutze für meine Seiten eine Art selbstgeschriebenes Framework; die selbe Codebase für alle Anwendungen, in der alles enthalten ist, was ich standardmäßig brauche. Session Handling, Anbindungen an Datenbanken, eine kleine Templateengine, etc.
Die Anwendungen an sich wurden bei mir bisher immer nur im „app“-Verzeichnis abgelegt. Hatte ich ein Update für eine Seite, habe ich das App-Verzeichnis überschrieben. Mussten Dateien gelöscht werden, habe ich entweder das ganze App-Verzeichnis gelöscht (oder nur die einzelnen Dateien, wenn ich Bock hatte in meine Commits zu gucken) und komplett neu hochgeladen.
Doch damit ist nun Schluss.
Vorab: was ist ein Phar-Archiv?
Phar steht für PHP-Archiv. Von daher sage ich eigentlich die ganze Zeit gerade immer „PHP-Archiv-Archiv“, aber das lassen wir mal außen vor. In einem Phar-Archiv können mehrere PHP-Dateien, Bilder, etc gebündelt werden – und sind dann, genau wie bei einem ZIP-Archiv, nur als eine Datei auf der Festplatte wiederzufinden.
Dies erleichtert den Down- und Upload auf entfernte Server natürlich um ein vielfaches (weil statt 400.000 kleiner Dateien nur eine Datei hochgeladen werden muss) und macht das ganze um ein vielfaches wartungsarmer (weil ich nicht mehr diesen bescheidenen App-Ordner permanent überschreiben und/oder löschen muss).
Wie sieht es mit der Performance aus?
Da war ich faul und habe gegoogelt, mich auf die Benchmarkes eines mir unbekannten weiteren Programmierers verlassen:
https://cweiske.de/tagebuch/php-phar-files.htm#benchmark
Zusammengefasst, falls die Seite down gehen sollte – statische CSS-/JS-Dateien sind langsamer im Phar, was verständlich ist, weil Apache die nicht direkt ausliefern kann. PHP-Dateien, die aus einer Phar geladen werden, sind hingegen (bei den meisten Konfigurationen) schneller – vermutlich, weil I/O-Operationen gespart werden.
Hätte ich nun spontan auch nicht gedacht, aber dann nehmen wir das mit.
Generelle Funktionsweise – in meinem Framework
Mein Framework kommuniziert mit der App über eine zentrale Schnittstelle; die Klasse Framework\ApplicationInterface
. Diese Klasse hat nur statische Methoden (weil pro Framework-Kopie nur eine Anwendung läuft) und macht folgendes:
- Beim Start des Frameworks prüft sie nach, ob eine app.phar existiert
- Falls ja bindet sie die als „app.phar“ ein und setzt ihre Variable „IsBundled“ auf true
- Falls nein macht sie nichts
Das Framework bindet dann eine Datei immer entweder aus dem /app-Ordner, oder aus der PHAR ein. Nirgendswo in meinem Framework wird direkt auf /app oder auf die PHAR zugegriffen, alles läuft über die ApplicationInterface.
PHP-Code im Framework – Teil 1
Der in der Liste oben beschriebene Ablaufcode sieht wie folgt aus:
<?php
/**
* Diese Klasse bietet ein Interface zur Ansteuerung der App
* Die lädt Daten entweder aus der PHAR, oder aus dem /app-Verzeichnis
*/
namespace Framework;
class AppInterface {
/**
* Wenn aus PHAR geladen wird, ist hier true.
* Sonst ist hier false
*/
private static $IsBundled = false;
/**
* Initialisiert das Interface. Checkt, ob PHAR, oder /app
* @param void
* @return void
*/
public static function Init() {
self::$IsBundled = file_exists(ROOT_DIR .'/app.phar');
\Util\Logger::D(self::$IsBundled ? 'This application was shipped as phar archive' : 'This application is running in standalone mode');
if(self::$IsBundled) \Phar::loadPhar(ROOT_DIR .'/app.phar', 'app.phar');
}
}
?>
Anmerkung: ich habe im Framework ROOT_DIR
definiert. Das zeigt auf das Wurzelverzeichnis, in welchem entweder die app.phar oder der /app-Ordner liegen.
PHP-Code Teil 2: Autoloading
In meinem Framework findet sich irgendwo folgender Code für autoloading:
spl_autoload_register(function($classname) {
$classname = str_replace('\\', '/', $classname);
if(file_exists(ROOT_DIR .'lib/'. $classname .'.class.php'))
require_once(ROOT_DIR .'lib/'. $classname .'.class.php');
if(\Framework\AppInterface::ContainsClass($classname))
\Framework\AppInterface::LoadClass($classname);
});
Der Code von Teil 1 ist um folgende zwei Funktionen erweitert:
/**
* Prüft, ob eine Klasse in der Anwendung existiert
* $ClassName muss hierbei bereits "/" statt "\" haben
* @param string $ClassName
* @return bool
*/
public static function ContainsClass($ClassName) {
if(self::$IsBundled) return file_exists('phar://app.phar/'. $ClassName .'.class.php');
else return file_exists(ROOT_DIR .'app/'. $ClassName .'.class.php');
}
/**
* Lädt eine Klasse aus der Anwendung
* $ClassName muss hierbei bereits "/" statt "\" haben
* @param string $ClassName
* @return void
*/
public static function LoadClass($ClassName) {
if(self::$IsBundled) require_once('phar://app.phar/'. $ClassName .'.class.php');
else require_once(ROOT_DIR .'app/'. $ClassName .'.class.php');
}
Dadurch funktioniert autoloading aus dem Phar-Archiv, sowohl in der Testumgebung als auch in der Produktiv-Umgebung
PHP-Code Teil 3: Innerhalb des Archives
Innerhalb des Archives findet sich eine autoconfig.php, in der die Anwendung sich selber initialisiert. Unter anderem wird dort eine APP_ROOT definiert, welche auf das Root-Verzeichnis der Anwendung zeigt:
define('APP_DIR', dirname(__FILE__));
Diese wird ebenfalls über die AppInterface (Code aus Teil 1) eingebunden:
/**
* Lädt die Routen aus der PHAR oder aus /app
* @param void
* @return void
*/
public static function LoadAutoconfig() {
if(self::$IsBundled) require_once('phar://app.phar/autoconfig.php');
else require_once(ROOT_DIR .'/app/autoconfig.php');
}
… und innerhalb des App-Verzeichnisses
Durch den autoconfig-Modus oben müssen wir uns nun um nichts mehr kümmern, was in der Anwendung passiert – solange wir Pfade mit APP_DIR
anfangen. Der Wert von APP_DIR lautet nämlich:
In der Testumgebung also /app als Verzeichnis | /var/www/demo/app |
In der Produktivumgebung also eine app.phar | /var/www/demo/app.phar |
Und damit funktioniert folgender Code sowohl in der Test-, als auch in der Produktivumgebung:
$CurrentVersion = \Util\Configuration::Get('System.Database.Version');
while(file_exists(APP_DIR .'/DatabasePatch/'. ($CurrentVersion + 1) .'.sql')) {
$CurrentVersion++;
$Patch = file_get_contents(APP_DIR .'/DatabasePatch/'. $CurrentVersion .'.sql');
\TR\Database::Update($Patch, array());
\Util\Configuration::Set('System.Database.Version', $CurrentVersion);
}
Bauen des Phar-Archives
Nun, da wir wissen, was ein Phar-Archiv ist, welche Vor- & Nachteile es hat sowie wie man mit einem Phar-Archiv kompatibel programmiert, wird es Zeit herauszufinden, wie man eines erstellt.
Dafür habe ich folgendes Script in der Hinterhand:
<?php
try
{
$target = '/.../app.phar';
if(file_exists($target)) unlink($target);
$phar = new Phar($target);
$phar->buildFromDirectory('/.../app');
$phar->stopBuffering();
echo 'ok';
}
catch (Exception $e) { print_r($e); }
?>
Per CMD (dran denken: PHP im PATH haben) und folgendem Befehl lässt es sich ausführen:
php.exe --define phar.readonly=0 -f build.php
Tada, schon liegt da eine app.phar im Verzeichnis und PHP nutzt die Dateien aus dieser.