Contexte
En début d'année 2025, j'ai développé un petit projet sur ATCD.
Je voulais renvoyer des réponses PSR-7 avec ma librairie de zip arnapou/zip.
Pour ce faire, je n'avais pas d'autres choix qu'utiliser les Fibres PHP pour y arriver.
PHP Fibers
Les Fibers ont été introduites en 2021 avec la version PHP 8.1.
On rappelle que PHP est monoprocess par défaut, mais grâce aux Fibers au sein de ce process,
on peut suspendre les instructions en cours pour exécuter une autre file d'instruction : une "fibre".
Exposé de manière simple, il est possible de passer des valeurs entre ces fibres et d'opérer une forme de "yoyo" entre plusieurs fibres et le process "main".

PSR-7: HTTP message interfaces
La PSR-7 permet de normaliser les objets de message HTTP depuis 2015,
et facilite donc l'interopérabilité des librairies et frameworks PHP.
La partie qui nous intéresse ici est principalement l'interface StreamInterface
qui permet de retourner un "body" de réponse HTTP en flux.
<?php
namespace Psr\Http\Message;
/**
* Extrait qui nous intéresse de
* l'interface de flux PSR-7.
*/
interface StreamInterface
{
public function read(int $length): string;
// ...
// Le reste ne nous intéresse pas pour les
// explications ci-dessous, suivez le lien
// pour l'interface complète :
// https://www.php-fig.org/psr/psr-7/
}
Du problème à la solution
Problème
-
D'un côté, j'ai un objet qui permet d'écrire un zip en flux :
PkwareWriter
en une fois. Ce writer stream-zip fait partie de ma librairie arnapou/zip. -
De l'autre, je veux avoir ce flux en respectant la PSR-7 avec un
StreamInterface
qui a une méthoderead($length)
qui doit retourner un morceau du flux zip.
Cela paraît contradictoire.
En effet, on pourrait simplifier en disant que le writer (1) pousse le flux zip, alors que le stream PSR-7 (2) doit tirer ce même flux.
C'est précisément ici qu'on peut tirer parti des Fibers.
Pré-requis
Dans le cas du writer (1), on doit pouvoir intercepter le flux écrit.
C'est là où me sert ma librairie arnapou/stream
qui contient deux interfaces principales Input
et Output
.
On n'a besoin pour cet article que de l'interface Output
.
<?php
namespace Arnapou\Stream\Output;
interface Output
{
public function write(string $data): void;
}
La classe PkwareWriter
utilise une implémentation de Output
injectée pour écrire son flux.
<?php
namespace Arnapou\Zip\Pkware;
use Arnapou\Stream\Output\Output;
final class PkwareWriter
{
public function __construct(Output $output)
{
}
}
✨ Yes ! On va enfin pouvoir commencer à jouer avec la solution.
Solution
StreamInterface::read()
.
La classe qui implémente StreamInterface
dans ma librairie arnapou/zip est nommée Prs7ZipResponseStream
.
Elle porte la liste des éléments à zipper.
Si vous avez besoin d'ajouter un fichier construit à la volée ou trop lourd, vous pouvez utiliser un élément
qui implémente l'interface ZippedInput
ou ZippedItem
selon vos besoins.
Exemple d'utilisation de Prs7ZipResponseStream
:
<?php
use Arnapou\Stream\Input\Input;
use Arnapou\Zip\Psr\Prs7ZipResponseStream;
use Arnapou\Zip\Writing\Zipped\ZippedFile;
use Arnapou\Zip\Writing\Zipped\ZippedItem;
use Arnapou\Zip\Writing\Zipped\ZippedInput;
$streamZip = new Prs7ZipResponseStream();
$streamZip->addZipItem(new ZippedFile($filename, $entryName));
$streamZip->addZipItem(new class implements ZippedInput {
public function getEntryName(): string {}
public function getInput(): Input {}
public function getTime(): int {}
});
$streamZip->addZipItem(new class implements ZippedItem {
public function getEntryName(): string {}
public function getContent(): string {}
public function getTime(): int {}
});
// Le stream peut être ajouté à votre réponse en utilisant
// votre implémentation préférée de PSR-7 ResponseInterface.
$response = new MyFavoritePsr7ResponseClass();
$response = $response->withBody($streamZip);
Pour la réponse, une classe utilitaire Prs7ZipResponse
a été créée pour simplifier la gestion des
headers communs Content-Type
, Cache-Control
, Last-Modified
, Content-Disposition
.
<?php
use Arnapou\Zip\Psr\Prs7ZipResponse;
$response = Prs7ZipResponse::create(
new MyFavoritePsr7ResponseClass(),
$filename,
$maxAge,
$lastModified,
);
$response->stream->addZipItem($item1);
$response->stream->addZipItem($item2);
// ...
return $response;
Code source complet :
Bilan
Les Fibres sont vraiment puissantes pour résoudre des problèmes d'architecture logicielle comme celui-ci.
J'ai utilisé également les Fibres dans ma lib arnapou/json-parser pour adapter
un pattern Visitor en Iterator.
Les Fibres peuvent rajouter un peu d'overhead (perte de performance) dans le switching de contextes.
Mais cela peut facilement être compensé par une activation du JIT qui a eu pour moi des effets notables.