Phar als PHP-Klassenbibliothek

Stand 2021-07-21
Autor: Wolfgang R. Schulz

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:

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:

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:

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";

[Zurück zum Inhaltsverzeichnis]