Streams – Ein kleines Beispiel
Stellen wir uns mal folgendes Problem vor. Wir haben einen Contentserver, auf dem alle meine Texte gespeichert sind. Jetzt habe ich einen Client, der diesen Inhalt nutzen will. Dummerweise ist die Verbindung zwischen Server und Client nicht die beste und somit sollte alles, was einmal abgerufen wurde auch beim Client gespeichert werden, um den nächsten Abruf zu beschleunigen.
Da ich schon immer mal was mit StreamWrappern bauen wollte, hier eine Lösung für das Problem, implementiert über Streams. Wie immer ist es nur ein Proof of Concept und kann an vielen Stellen noch verbessert werden. Es funktioniert aber schon und kann als Inspiration für viele weitere Streams gelten.
Wenn ich auf den gewünschten Inhalt zugreife, dann hätte ich das gerne so bequem wir möglich. Das beste wäre, wenn ich noch nicht mal wissen muss, dass es einen Cache gibt. Noch besser wäre es natürlich, wenn ich nicht mal wissen müsste, dass es ein Server ist, der mir das ganze zur Verfügung stellt. Ich schreibe hier natürlich nur Anforderungen auf, für die ich auch eine Lösung habe. Gemein, oder? Wie will ich also den Aufruf gestalten. Ich stelle mir das so vor:
$text = file_get_contents( "content://1.txt" ) );
Um so einen Aufruf möglich zu machen, muss man in PHP eigentlich nur ein paar Zeilen Code schreiben und schon klappt es. 45 sind es, wobei da nur 13 wirklich geschrieben sind und die anderen einfach nur delegieren. Schauen wir uns den Code doch einfach mal an:
class ContentStream
{
static public $localPath;
static public $remotePath;
private $handle;
function stream_open($path, $mode, $options, &$opened_path)
{
$pathArray = parse_url($path);
$filename = $pathArray['host'];
$remoteFileName = self::$remotePath.DIRECTORY_SEPARATOR.$filename;
$localFileName = self::$localPath.DIRECTORY_SEPARATOR.$filename;
if ( !file_exists( $localFileName ) )
{
file_put_contents( $localFileName, file_get_contents( $remoteFileName ) );
}
$this->handle = fopen( $localFileName, $mode );
return true;
}
function stream_read($count)
{
return fgets( $this->handle, $count );
}
function stream_eof()
{
return feof( $this->handle );
}
function stream_stat( )
{
return fstat($this->handle);
}
}
Eigentlich brauch der Code keine Erklärung. Alle Methode außer stream_open leiten die Anfrage einfach an das native Handling von PHP weiter. Im stream_open schaue ich einfach, ob die Datei lokal existiert, falls nicht, dann hole sie und setze den Handle auf das Teil. Um den Stream zu „konfigurieren“ und registrieren fehlen nur noch folgende drei Zeilen:
ContentStream::$localPath = 'local';
ContentStream::$remotePath = 'remote';
stream_wrapper_register("content", "ContentStream");
Wie vorhin gesagt: Der Code ist in 15min entstanden. Es muss an vielen Stellen verbessert werden, aber er funktioniert für das kleine Beispiel. Ich hoffe, ihr fühlt euch inspiriert und baut den weltbesten Stream.
Nettes Beispiel!
Sobald du aber etwas mehr machen willst als file_get_contents() musst du noch mehr von den StreamWrapper Methoden implementieren. Ich wünschte, dafür gäbe es ein oder mehrere Interfaces von Seiten PHP aber Pustekuchen..
Ich hab in einem symfony Plugin [1] verschiedene Bildquellen (Filesystem, Datenbank über Doctrine, über Propel und via HTTP) über Streams realisiert. Funktioniert ganz hervorragend und ist super transparent! Wer Interesse hat, kann sich das ja mal anschauen.
[1] http://www.symfony-project.org/plugins/sfImageTransformExtraPlugin
Ja über die Interface-Geschichte haben wir uns ja schon öfters unterhalten. Ob es jedoch was bringt selbst Interfaces zu implementieren müsste man mal rausfinden. Es validiert ja niemand, wenn man den Wrapper registriert. Vielleicht noch eine eigene Register-Funktion.
Aber Christian hat recht, wer mehr Funktionalität will, der muss noch ein paar Methoden implementieren, da ich nicht mehr wollte in meinem POC war das so ok.
Um die Existenz der stream_wrapper_register() Funktion wusste ich Bescheid. Anstatt neue Streams zu registrieren habe ich bisher eigene Stream Klassen implementiert, die ähnlich den Streams von Java/Dotnet waren (ja, abgeguckt 😛 ). Das hatte den Vorteil, dass ich OO programmieren konnte und nicht schon wieder mit Funktionen arbeiten musste. Normalerweise wrappt man Funktionen in Klassen, aber umgekehrt finde ich es unschön.
Da ich für die Stream Klassen gegen Interfaces programmiert habe, konnte ich problemlos einen StringStream (MemoryStream) und andere Sorten von Streams basteln. Auch einen Teil-Stream aus einem anderen „IStream“ zu bauen fand ich recht nützlich.
Grüße
@maz
Zugegeben die PHP Dateifunktionen haben mit OO nichts am Hut, aber sie sind recht verbreitet. In meinem Beispiel oben habe ich es zB mit einem fremden Plugin zu tun gehabt, das viel mit diesen Funktionen macht.
Nur mit Streams konnte ich mich hier vom FileSystem loesen ohne das fremde Plugin zu patchen
@Nils
Schade, dass ausgerechnet dieses Thema scheinbar nicht auf sehr viel Interesse trifft (der Menge an Kommentaren folgend). Ich stimme das Sebastian Bergmann zu. StreamWrapper und StreamFilter sind vielleicht die zwei am meisten unterschätzten Features, die PHP zu bieten hat.