Menu in caricamento...

PHP Stream Wrappers e black magic

DigitalSoftwarePhpStream wrappers
2019 M03 5
A cena col boss: lo StreamWrapper
Chiunque di noi abbia mai avuto a che fare con PHP si sarà trovato almeno una volta a interagire con dei file.
Per leggere un file temporaneo, per salvare un dato in cache, o per salvare un'immagine su disco.

Per esempio, possiamo leggere i primi 128 caratteri del contenuto del file /tmp/file così:
                                
$fp = \fopen('/tmp/file', 'rb');
echo \fread($fp, 128);
                                
                            
Oppure:
                                
$fp = \fopen('file:///tmp/file', 'rb');
echo \fread($fp, 128);
                                
                            

Ma che cosa significa file://?
Il concetto di stream
Uno stream in informatica, ma più precisamente nel contesto dei sistemi UNIX-based o C-based, può essere pensato come uno spostamento di dati da un punto a un altro.
Per esempio, possiamo parlare di stream per una connessione TCP o UDP, uno spostamento di dati verso un file server, lettura o scrittura su pipe di sistema (STDIN e STDOUT) e anche per una semplice lettura da file.

Per semplificare all'estremo, possiamo immaginare uno stream come un'operazione che contempli lettura e scrittura.
Poco importa che tali azioni avvengano su supporti separati.
Stream wrappers
Possiamo intuirne il funzionamento traducendo letteralmente dall'inglese: un wrapper è un contenitore, più o meno astratto, che ci dà accesso a uno stream, per semplificarci (basta crederci :D) il lavoro di interagire con esso.

Il wrapper file:// ci permette di interagire con il file system.
Questo wrapper è implicitamente utilizzato da PHP ogni qualvolta si vada a effettuare un'operazione su file.
Intuitivamente, il wrapper http:// ci permette di interagire con lo stack HTTP, e il wrapper ftp:// ci permette di comunicare con altri sistemi via File Transfer Protocol.

(Hint: esiste lo stream wrapper zip://. Come nei migliori manuali di Fisica I, l'onere della prova, nel senso di "test", è lasciato al lettore).
Tutto molto bello. Ma poi?
Se compresi e usati in modo opportuno, gli stream wrappers diventano uno strumento molto potente e versatile.
La versatilità degli stream wrapper risiede nel fatto che PHP, oltre a mettercene a disposizione una discreta quantità, ci permetta di registrarne a piacere.
Woah! Come???
Semplice: è sufficiente "implementare" una piccola interfaccia virtuale, per costruire il nostro StreamWrapper.
Possiamo utilizzarlo, per esempio, per interagire con un protocollo proprietario.
E-sem-pio! E-sem-pio!
Here you go: inventiamo un piccolo protocollo al volo.
Questo protocollo sarà JSON-based, e sarà identificato dallo schema jsonkey://. Iniziamo a implementare il nostro StreamWrapper, aggiungendo le funzioni necessarie a interagire con questo stream!
Teniamo a portata di mano la documentazione dell'interfaccia virtuale sul sito ufficiale di PHP.

Iniziamo a costruire la nostra classe:
                                
<?php
// Nota: l'interfaccia StreamWrapper è "virtuale", non è una vera e
// propria interfaccia nel senso attribuitole nell'Object-Oriented Programming.
// Pensiamola come un'implementazione di riferimento per uno StreamWrapper.
class JsonKeyWrapper
{
    /**
     * @var resource Per conservare il valore della `resource` restituita da `fopen()`
     */
    protected $handle;

    /**
     * @var string Questa è la chiave del JSON a cui vogliamo accedere.
     */
    protected $key;

    /**
     * @var bool Questa variabile ci sarà utile solamente in questa simulazione,
     *           per forzare il termine del processo di lettura.
     */
    protected $firstRun = true;
}
                                
                            

L'obiettivo è poter effettuare due semplici operazioni:
  1. Leggere il contenuto della chiave "a" dal file /tmp/example.json.
                                            
    $fp = \fopen('jsonkey:///tmp/example.json?key=a', 'rb');
    echo \fread($fp, 128) . \PHP_EOL;
                                            
                                        
  2. Creare la chiave "b" nel file /tmp/example.json.
                                            
    $fp = \fopen('jsonkey:///tmp/example.json?key=b', 'rb+');
    echo \fwrite($fp, 'test') . \PHP_EOL;
                                            
                                        
Per permettere a \fopen() di funzionare correttamente, abbiamo bisogno di "implementare" il metodo \stream_open() dell'interfaccia sopraindicata.
                                
public function stream_open(
    string $path,
    string $mode,
    int $options,
    string &$opened_path = null
): bool {
    // Decomponiamo la path fornita con '\parse_url()',
    // ma rimuoviamo lo scheme 'jsonkey://' dalla path,
    // per permettere a \parse_url() di raccapezzarcisi.
    $parts = \parse_url(\substr($path, 10));
    $filePath = $parts['path'];

    // Preleviamo la query dall'URI ed estraiamo la chiave "key":
    // questa è la chiave del JSON a cui vogliamo accedere.
    \parse_str($parts['query'], $query);
    $this->key = $query['key'];
    // Salviamoci la file handle nella proprietà dichiarata precedentemente...
    $this->handle = \fopen($filePath, $mode);
    // ... e ritorniamo 'true' o 'false' per indicare 'success' o 'failure'
    return !empty($this->handle);
}
                                
                            
Inoltre, dovremo poter chiamare \fread() e \fwrite(), per le quali avremo bisogno rispettivamente di:
                                
public function stream_eof(): bool
{
    // Ecco l'utilità della nostra variabile: ci serve emulare EOF!
    return $this->firstRun === false;
}

public function stream_read(int $count): string
{
    if (!$this->firstRun) {
        // Se abbiamo già letto la chiave desiderata, terminiamo l'operazione
        // di lettura, simulando di fatto un EndOfFile.
        return '';
    }

    $this->firstRun = false;
    // Leggiamo direttamente tutto il contenuto dello stream, per poter
    // estrarre la chiave specifica richiesta.
    $content = \json_decode(\stream_get_contents($this->handle), true);

    return $content[$this->key];
}

public function stream_write(string $data): int
{
    // Analogamente a sopra, leggiamo direttamente tutto il contenuto dello
    // stream, per poter aggiungere la chiave fornita.
    $content = \json_decode(\stream_get_contents($this->handle), true);
    $content[$this->key] = $data;

    // Ricordiamoci di utilizzare 'fseek()' per tornare all'inizio del nostro
    // stream prima di scriverci, o le conseguenze saranno piuttosto spiacevoli
    // per il nostro JSON!
    // Commentare la riga seguente per credere.
    \fseek($this->handle, 0, \SEEK_SET);

    // Piccolo trick: `\fwrite()` conta la quantità di byte trascritta sullo
    // stream di destinazione.
    // Noi, lavorando con un file JSON, dobbiamo per forza sovrascrivere
    // l'intero file di volta in volta, per aggiungere una chiave.
    // Onde evitare warnings, restituiamo la quantità di byte fornita (e
    // quindi, in teoria, aggiunta) anziché direttamente il risultato della
    // chiamata a funzione.
    return !\fwrite($this->handle, \json_encode($content))
        ? 0
        : \strlen($data);
}
                                
                            
All'opera!
Dovremmo avere tutto il necessario per poter far funzionare il nostro wrapper!
Creiamo il file /tmp/example.json, e aggiungiamoci del contenuto:
                                
{
  "a": "Value A"
}
                                
                            

All'opera con il wrapper!
                                
<?php
\stream_wrapper_register('jsonkey', 'JsonKeyWrapper');

// Leggiamo il contenuto della chiave "a"
$fp = \fopen('jsonkey:///tmp/example.json?key=a', 'rb');
$a = \fread($fp, 128); // $a === "Value A"

// Oppure creiamo la chiave "b" con contenuto "Value B"
$fp = \fopen('jsonkey:///tmp/example.json?key=b', 'rb+');
\fwrite($fp, 'Value B');
                                
                            
Ok, interessante, ma le applicazioni pratiche?
Per il momento, ci siamo limitati a introdurre il concetto di StreamWrapper, con una breve spiegazione per capirne un po' meglio le logiche di funzionamento.

Purtroppo, PHP stesso documenta questa funzionalità poco e, a nostro parere, in modo troppo semplicistico.
Questa serie di articoli ci permetterà di scoprire le potenzialità nascoste degli StreamWrappers, coprendone sì gli aspetti tecnici, ma sopratutto quelli pratici.

Nel prossimo articolo, andremo a esplorare il concetto di stream filter, analizzandone alcune semplici applicazioni.
Andremo a implementare un sistema che ci permetta di leggere e scrivere da uno stream cifrato, con il minimo consumo di risorse e tempo di esecuzione.

Stay tuned!