PHP Stream : wrappers, filters... un allié méconnu

PHP Stream : wrappers, filters... un allié méconnu


Définition

La définition du manuel étant déjà très claire, je me contente simplement de vous la partager.

La gestion des flux a été introduite en PHP 4.3.0. comme méthode de généralisation des fichiers, sockets, connexions réseau, données compressées et autres opérations du même type, qui partagent des opérations communes. Dans sa définition la plus simple, un flux est une ressource qui présente des capacités de flux. C'est-à-dire que ces objets peuvent être lus ou recevoir des écritures de manière linéaire, et disposent aussi de moyens d'accéder à des positions arbitraires dans le flux.

Protocoles

Un protocole est une spécification de plusieurs règles pour un type de communication. Il peut également être utile pour vérifier que les informations soient correctement reçues.

Dans une conversation téléphonique quand l'interlocuteur décroche, il commence par dire "Allô" afin de spécifier qu'il est prêt à recevoir des informations.

La référence d'un flux (style URL) s'écrit de la forme scheme://target.

Donc oui https://blog.eleven-labs.com/ peut être ouvert comme un flux qui pointe vers une ressource distante.

Wrappers

Dans le contexte présent un wrapper est un gestionnaire de protocole (de style URL).

Voici la liste des scheme (wrappers) supportés par PHP :

  • file:// — Accès au système de fichiers local
  • http:// — Accès aux URLs HTTP(s)
  • ftp:// — Accès aux URLs FTP(s)
  • php:// — Accès aux divers flux I/O
  • data:// — Données (RFC 2397)
  • glob:// — Trouve des noms de fichiers correspondant à un masque donné
  • phar:// — Archive PHP (PHP >= 5.3.0)
  • zlib:// — Flux de compression (requiert un extension pour zip://)
  • ssh2:// — Shell sécurisé 2 (requiert l'extension SSH2)
  • rar:// — RAR (requiert l'extension RAR)
  • ogg:// — Flux Audio (requiert l'extension OGG/Vorbis)
  • expect:// — Flux d'interactions de processus (requiert l'extension Expect)

Utiliser stream_get_wrappers() pour avoir la liste des protocoles supportés par votre serveur.

var_dump(stream_get_wrappers());

Transports

Un transport en PHP ce n'est ni plus ni moins qu'un moyen de transférer des données. Pour cela PHP utilise les sockets.

ℹ️ Il ne faut pas oublier que les sockets sont aussi des flux 😜.

Sockets type WEB

Utiliser stream_get_transports() pour avoir la liste des protocoles de transport supportés par votre serveur.

var_dump(stream_get_transports());

À noter que les paths des sockets web s'écrivent sous la forme {PROTOCOL}://{DOMAIN / IP v4, v6}:{PORT}

Voici plusieurs exemples :

Sockets type UNIX

  • unix:// (fournit l'accès à un flux de type socket, sur un domaine Unix)
  • udg:// (fournit un mode de transport alternatif, avec un protocole de datagrammes utilisateur)

Voila un petit tour d'horizon des différents protocoles que PHP met nativement, ou par extensions, à votre disposition.

👨‍🚀 Il est également possible de créer sont propre wrapper, afin d'encapsuler la logique de transmission des données !

Comme par exemple :

  • s3:// donne accès à votre storage AWS
  • git:// permet d'intéragir avec git
  • hoa:// permet d'accéder aux différentes informations managées par HOA

Contexte de flux

Les contextes de flux sont une autre notion importante de la gestion des flux. Le contexte est un ensemble d'options qui sera passé en argument aux diverses fonctions de traitements de flux (ex: stream_*, fopen, copy, file_get_contents...).

$context = stream_context_create( [ 'http' => [ 'protocol_version' => '1.1', 'timeout' => 10, 'user_agent' => 'Wilson Browser', 'method' => 'GET', ], ], [] ); $result = file_get_contents('http://../page', false, $context);

La requête générée pour récupérer la page sera donc en GET HTTP 1.1 avec un user agent Wilson Browser et un timeout à 10 secondes.

Vous pouvez également utiliser stream_context_set_default afin de configurer les options par défaut des gestionnaires de flux.

stream_context_set_default([ 'http' => [ 'timeout' => 10, 'user_agent' => 'Wilson Browser', ], 'ftp' => [...] ]);

⚠️ Attention à l'utilisation de cette dernière, car elle configure les options de toutes les requêtes HTTP faites par la couche de flux de PHP.

Filtres

Une autre partie assez intéressante des flux étant la possibilité d'ajouter des fonctions de filtre sur les données qui transiteront dans le flux.

  • string.rot13
  • string.toupper
  • string.tolower
  • string.strip_tags (depuis PHP 5)
  • convert.base64-encode - convert.base64-decode
  • convert.quoted-printable-encode - convert.quoted-printable-decode
  • zlib.deflate (compression) (PHP >= 5 si le support zlib est activé)
  • zlib.inflate (decompression) (PHP >= 5 si le support zlib est activé)
  • _bzip2.compress (PHP >= 5 si le support bz2 est activé)
  • bzip2.decompress (PHP >= 5 si le support bz2 est activé)
  • mcrypt.* (❌ OBSOLETE depuis PHP 7.1.0. Nous vous encourageons vivement à ne plus l'utiliser.)
  • mdecrypt.* (❌ OBSOLETE depuis PHP 7.1.0. Nous vous encourageons vivement à ne plus l'utiliser.)

Utiliser stream_get_filters() pour avoir la liste des filtres supportés par votre serveur.

var_dump(stream_get_filters());

Il existe 2 syntaxes pour configurer un filtre sur un flux.

L'utilisation de stream_filter_append/stream_filter_prepend.

$fp = fopen('php://output', 'w'); stream_filter_append($fp, 'string.toupper', STREAM_FILTER_WRITE); fwrite($fp, "Code de lancement: 151215"); fclose($fp);

Exécuter le php

Grâce au flux php://filter

file_put_contents('php://filter/string.toupper/resource=php://output', 'Code de lancement: 151215');

Exécuter le php

Les 2 exemples ci-dessus vont afficher CODE DE LANCEMENT: 151215

👨‍🚀 Là aussi il est possible de créer son propre filter grâce à php_user_filter !

Voici un petit filtre geek.

class l33t_filter extends php_user_filter { function filter($in, $out, &$consumed, $closing) { $common = ["a", "e", "s", "S", "A", "o", "O", "t", "l", "ph", "y", "H", "W", "M", "D", "V", "x"]; $leet = ["4", "3", "z", "Z", "4", "0", "0", "+", "1", "f", "j", "|-|", "\\/\\/", "|\\/|", "|)", "\\/", "><"]; while ($bucket = stream_bucket_make_writeable($in)) { $bucket->data = str_replace($common, $leet, $bucket->data); $consumed += $bucket->datalen; stream_bucket_append($out, $bucket); } return PSFS_PASS_ON; } } stream_filter_register('l33t_filter', 'l33t_filter') or die('Failed to register filter Markdown'); file_put_contents('php://filter/l33t_filter/resource=php://output', 'Salut ça va?');

Exécuter le php

L'exemple du dessus convertira Salut ça va? en Z41u+ ç4 v4?

On peut imaginer des filtres html>markdown, un emoji converter, un dictionnaire de mots blacklistés, etc.

Les flux I/O

PHP met également à notre disposition des flux d'Input/Output.

php://stdin

C'est le flux d'entrée standard (ligne de commande)

ℹ️ stdin: est en lecture seule

Exemple

//index.php copy( 'php://stdin', 'php://filter/string.toupper/resource=php://stdout' );

La commande ci-dessous écrira string dans le flux stdin et ici on copie simplement ce que l'on reçoit dans la sortie standard après avoir appliqué un filtre toupper.

$ echo 'string' | php index.php #affichera STRING $ cat file.txt | php index.php #affichera le contenu du fichier en majuscule

php://stdout et php://stderr

Sont les flux de sortie standards (ligne de commande)

ℹ️ stdin: est en lecture seule

Exemple

//error.php error_reporting(E_ALL); ini_set("display_errors", 0); echo 'Hello '.$_GET['user'];

Avec ce script nous allons reporter toutes les erreurs (E_ALL) mais ne pas les afficher aux visiteurs.

Dans un navigateur web ce script affichera :

Hello

Et les erreurs seront dirigées vers le flux php://stderr qui est bien souvent configuré par votre file handler (nginx/apache...) grâce au paramètre error_log.

👨‍🚀 En ligne de commande php://output php://stderr sont par défaut envoyés dans php://stdout

Lançons ce script avec la commande suivante :

$ php error.php

Ce qui donnera :

PHP Notice:  Undefined index: user in /var/www/error.php on line 5
PHP Stack trace:
PHP   1. {main}() /var/www/error.php:0
Hello %

Utilisons maintenant la redirection de flux GNU/Linux

$ php error.php > out.txt

la console affichera :

PHP Notice:  Undefined index: user in /var/www/error.php on line 5
PHP Stack trace:
PHP   1. {main}() /var/www/error.php:0

tandis que le fichier out.txt contiendra :

Hello

Mais on peut également rediriger la sortie d'erreur

$ php error.php 2> errors.txt

la console affichera :

Hello

tandis que le fichier errors.txt contiendra :

PHP Notice:  Undefined index: user in /var/www/error.php on line 5
PHP Stack trace:
PHP   1. {main}() /var/www/error.php:0

ℹ️ On peut également combiner les 2 php error.php > out.txt 2> errors.txt

> et 2> écrase le fichier ou le crée. >> et 2>> écrit à la fin du fichier ou le crée. 2>&1 et 2>>&1 redirige les 2 flux (avec le même comportement pour > et >>)

php://input

Permet de lire les données brutes du corps de la requête.

⚠️ N'est pas disponible avec enctype="multipart/form-data".

php://output

Permet d'écrire dans le buffer de sortie de la même façon que print echo.

// les 2 écritures suivantes feront la même chose file_put_contents('php://output', 'Some data'); echo 'Some data';

N'oubliez pas qu'avec php://output vous pouvez utiliser les filtres, le contexte et même pourquoi pas réécrire au début.

php://temp et php://memory

Permet d'écrire dans un gestionnaire de fichiers. php://memory stockera toujours en mémoire tandis que php://temp stockera en mémoire, puis sur disque après avoir attendu la limite prédéfinie (défaut 2Mo)

👨‍🚀 php://temp/maxmemory:200 stockera sur disque une fois que 200 octets seront écrit dans le flux.

php://filter

Permet d'ajouter un filtre lors de l'ouverture d'un autre flux. Exemple :

// Applique un filtre lors de la lecture file_get_contents('php://filter/read=string.rot13/resource=example.txt'); // Applique un filtre lors de l'écriture file_put_contents('php://filter/write=string.rot13/resource=example.txt', 'new data'); // Applique le filtre lors de l'écriture mais aussi lors de la lecture $res = fopen('php://filter/string.rot13/resource=example.txt', 'w+');

php://fd

N'ayant pas trouvé d'informations utiles je vous laisse consulter la documentation

Quelques cas d'utilisation

Prenons l'exemple d'une copie de fichier :

$file = file_get_contents('http://.../textfile.txt'); file_put_contents(__DIR__.'/downloaded_textfile.txt', $file);

⚠️ Avec ce code nous allons télécharger entièrement le fichier textfile.txt dans la mémoire avant de l'écrire dans le fichier de destination !

Maintenant si l'on change légèrement le code on obtient :

DO

copy( 'http://.../textfile.txt', __DIR__.'/downloaded_textfile.txt' );

ℹ️ On peut faire le même traitement avec des ressources :

stream_copy_to_stream( fopen('http://.../textfile.txt', 'r'), fopen(__DIR__.'/downloaded_textfile.txt', 'w+') );

Voici la consommation mémoire pour un fichier de 5Mo.

Codememory_get_usage(true)memory_get_peak_usage(true)
file_get_content8Mo13Mo
copy2Mo2Mo
stream_copy_to_stream2Mo2Mo

La différence de consommation mémoire est due au fait que copy et stream_copy_to_stream vont directement écrire la source dans la destination.

👨‍🚀 N'hésitez pas à utiliser les wrappers/transports cités au début de l'article.


copy( 'http://.../image.jpg', 'ssh2.scp://user:pass@server:22/home/download/image.jpg' );

Copie le fichier depuis le web sur un serveur en utilisant scp via ssh.


Un autre exemple fréquemment rencontré lors de la création de fichier temporaire :

$tmpFile = tempnam(sys_get_temp_dir(), 'php' . rand());

⚠️ Ici le script va créer un fichier dans le dossier temporaire de php.

  • Ce qui veut dire qu'il vous faudra supprimer vous-même ce fichier.
  • La fonction tempnam() retourne le path et non la ressource.

👨‍🚀 Préférez donc l'utilisation de :

  • php://temp ou php://temp/maxmemory:100 qui stockera en mémoire puis sur disque une fois la limite atteinte.
  • php://memory stocker en mémoire
  • tmpfile() crée un fichier temporaire avec un nom unique, ouvert en écriture et lecture (w+), et retourne un pointeur de fichier.
$tmp = fopen('php://temp', 'w+');

Ce fichier sera automatiquement effacé :

  • lorsqu'il sera fermé.
  • lorsqu'il n'y a plus de référence au gestionnaire de fichiers.
  • lorsque le script sera terminé.

Conclusion

Bien que très puissant et présent dans PHP depuis la version 4.3, ce composant est souvent méconnu, sous exploité, voire mal utilisé. C'est pourquoi j'en fais la promotion ici. J'espère avoir suscité votre intérêt !

📝 Je n'ai volontairement pas abordé les flux de type socket car, ils mériteraient un article à eux seuls.

Liens utiles

Auteur(s)

Anthony MOUTTE

Anthony MOUTTE

_Développeur / Concepteur @ ElevenLabs_🚀 je suis très intéressé par la recherche et développement ainsi que les bonnes pratiques.

Voir le profil

Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !

Notre équipe d'experts répond à toutes vos questions.

Nous contacter

Découvrez nos autres contenus dans le même thème

Astronaute revenant de mission

Retour sur le Forum PHP 2024

Découvrez un résumé concis des conférences qui nous ont le plus marqué lors du Forum PHP 2024 !