Cas d'usage de Fiber en PHP

Construire une réponse PSR-7 qui renvoie un flux ZIP (Septembre 2025)

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

Schéma sur les Fibers (source PHP RFC)
Source : PHP RFC Fibers

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

  1. 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.
  2. De l'autre, je veux avoir ce flux en respectant la PSR-7 avec un StreamInterface qui a une méthode read($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

Schéma sur les Fibers (source PHP RFC)
Pseudo diagramme de séquence de la mise en œuvre de l'utilisation de fibre dans la méthode 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.