La plupart des projets PHP qui appellent une API manipulent des requêtes et des réponses HTTP. Ces objets viennent d'une bibliothèque, souvent Guzzle ou Nyholm, et chacune fournit ses propres classes.
Le jour où l'équipe change de bibliothèque, ou veut tester son code sans dépendre de l'une d'elles, chaque new disséminé dans le code devient un endroit à retrouver et à modifier. La requête, la réponse et leurs flux forment un ensemble qui doit changer d'un bloc.
Le blocage tient à la dépendance directe envers des classes concrètes appartenant à une même famille. La troisième case du tableau périodique des design patterns, la Fabrique abstraite, existe pour regrouper ces créations derrière une interface et rendre la famille remplaçable en un seul endroit.
Cet article montre ce pattern dans l'écosystème PHP, où il a pris la forme d'un standard, PSR-17. Il s'adresse aux personnes en reconversion, aux développeurs juniors et aux curieux qui commencent à lire du code PHP orienté objet.
Le problème : une famille de classes concrètes dispersée dans le code
Voici une requête construite directement avec les classes de Guzzle, présentée comme version « avant ». Le constructeur de Request et la méthode Utils::streamFor() existent bien dans la bibliothèque, et ce code fonctionne.
<?php
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
$corps = json_encode(['ville' => $ville, 'message' => $message], JSON_THROW_ON_ERROR);
$requete = new Request(
'POST',
'https://api.exemple.fr/alertes',
['Content-Type' => 'application/json'],
Utils::streamFor($corps),
);
Multiplié par vingt appels répartis dans un projet, ce code attache chaque fichier à Guzzle. Passer à Nyholm, plus légère, demande de réécrire chaque construction, et chaque réécriture est une occasion d'oublier un en-tête. Les tests unitaires subissent le même couplage : impossible de vérifier ce que produit ce code sans charger la bibliothèque précise qu'il nomme.
Le défaut est plus large que celui traité par la Fabrique, qui fait varier un seul type d'objet. Ici, plusieurs types vont ensemble et doivent changer en même temps. Une requête Guzzle accompagnée d'un flux Nyholm fonctionne ici grâce au standard PSR-7, mais dans une famille sans standard commun, mélanger les pièces de deux fournisseurs produit des objets incompatibles.
Le principe : une interface qui crée toute la famille
La Fabrique abstraite du Gang of Four déclare une interface avec une méthode de création par type de produit. Chaque fabrique concrète implémente toutes ces méthodes pour une famille donnée. Le code client reçoit une fabrique, appelle ses méthodes et ne nomme jamais une classe concrète. Changer de famille revient à lui passer une autre fabrique.
L'exemple du livre de 1994 portait sur des interfaces graphiques : une fabrique produisait les fenêtres et les barres de défilement d'un même style visuel, pour qu'une application ne mélange pas deux apparences. Le principe n'a pas changé, seul le domaine diffère.
Les signes qui annoncent le besoin
Le pattern ne se décide pas sur le papier. Quelques indices, visibles dans un projet existant, signalent qu'une famille de classes concrètes mérite d'être cachée derrière une fabrique.
- Les mêmes imports partout. Le même espace de noms d'un fournisseur, par exemple
GuzzleHttp\Psr7, apparaît en tête de dizaines de fichiers qui n'ont rien à voir avec le réseau. - Des tests qui chargent le fournisseur. Un test unitaire censé vérifier une règle métier doit installer et configurer une bibliothèque HTTP pour s'exécuter.
- Deux familles en même temps. La production utilise un fournisseur, l'environnement de démonstration ou les tests en utilisent un autre, et le code se remplit de conditions sur l'environnement.
- Un changement de fournisseur à l'étude. Une migration est envisagée, et l'estimation du travail se compte en fichiers à modifier plutôt qu'en une ligne de configuration.
Un seul de ces signes ne suffit pas toujours à justifier le pattern. Quand plusieurs se cumulent, la fabrique rapporte plus qu'elle ne coûte, surtout si une interface standard existe déjà pour la famille concernée.
PSR-17 : le pattern devenu standard
En PHP, la recommandation PSR-17 du PHP-FIG décrit un standard commun pour les fabriques qui créent les objets HTTP de PSR-7. Elle définit six interfaces, une par produit de la famille.
- `RequestFactoryInterface`. crée une requête sortante avec
createRequest($method, $uri). - `ResponseFactoryInterface` et `ServerRequestFactoryInterface`. créent une réponse et une requête côté serveur.
- `StreamFactoryInterface`. crée un flux à partir d'une chaîne, d'un fichier ou d'une ressource PHP.
- `UploadedFileFactoryInterface` et `UriFactoryInterface`. créent un fichier téléversé et une URI.
Deux bibliothèques courantes implémentent la famille complète dans une seule classe. Leur code source le montre directement : Nyholm\Psr7\Factory\Psr17Factory et GuzzleHttp\Psr7\HttpFactory déclarent toutes deux implémenter les six interfaces. Symfony s'appuie sur ce standard : pour utiliser son client compatible PSR-18, sa documentation demande d'installer une implémentation PSR-17, soit nyholm/psr7, pour lequel Symfony Flex fournit des alias d'autowiring, soit php-http/discovery pour détecter une implémentation déjà présente.
Symfony applique même le pattern dans l'autre sens. Sa documentation indique que la classe Psr18Client, qui adapte le client HTTP de Symfony à l'interface PSR-18, implémente aussi les méthodes utiles de PSR-17 pour faciliter la création des requêtes. Le client sert donc à la fois à envoyer les requêtes et à les fabriquer, avec createRequest() appelé directement sur lui. Un même objet peut ainsi appartenir à plusieurs rôles, tant qu'il respecte les interfaces attendues.
Mon avis sur ce point : PSR-17 est l'un des meilleurs supports pour apprendre ce pattern, justement parce qu'il ne ressemble pas à un exercice. Les interfaces et leurs implémentations se lisent en quelques minutes, et le bénéfice se mesure le jour où l'on remplace une bibliothèque.
Exemples
PHP 8.5 : un client qui ne connaît aucune bibliothèque
Le code suivant cible PHP 8.5, la branche stable la plus récente au moment où ces lignes sont écrites. Il n'utilise rien de propre à cette version, puisque les classes readonly existent depuis PHP 8.2 et les propriétés promues depuis PHP 8.0. Il a été relu ligne à ligne contre les interfaces PSR-17 et contrôlé avec php -l. L'exécution avec les bibliothèques n'a pas pu être lancée dans l'environnement de rédaction.
<?php
declare(strict_types=1);
namespace App\Alertes;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
final readonly class ClientAlertes
{
public function __construct(
private RequestFactoryInterface $requetes,
private StreamFactoryInterface $flux,
) {
}
public function creerAlerte(string $ville, string $message): RequestInterface
{
$corps = json_encode(['ville' => $ville, 'message' => $message], JSON_THROW_ON_ERROR);
return $this->requetes
->createRequest('POST', 'https://api.exemple.fr/alertes')
->withHeader('Content-Type', 'application/json')
->withBody($this->flux->createStream($corps));
}
}
La classe demande deux produits de la famille, une requête et un flux, par leurs interfaces. Les mots Guzzle et Nyholm n'apparaissent nulle part. Le constructeur reçoit deux arguments distincts plutôt qu'une fabrique unique, ce qui limite la dépendance aux seules capacités utilisées.
L'assemblage : le seul endroit qui connaît la famille
La famille concrète se choisit là où l'application assemble ses objets. Comme les deux bibliothèques regroupent toute la famille dans une seule classe, le même objet remplit les deux paramètres.
<?php
use App\Alertes\ClientAlertes;
use GuzzleHttp\Psr7\HttpFactory;
use Nyholm\Psr7\Factory\Psr17Factory;
// Famille Nyholm
$fabrique = new Psr17Factory();
$client = new ClientAlertes($fabrique, $fabrique);
// Famille Guzzle : une seule ligne change
$fabrique = new HttpFactory();
$client = new ClientAlertes($fabrique, $fabrique);
Dans une application Symfony, cet assemblage disparaît du code applicatif. Avec nyholm/psr7 installé, les alias d'autowiring permettent au conteneur d'injecter la fabrique dans ClientAlertes à partir des seuls types déclarés dans le constructeur. Changer de famille devient une affaire de configuration.
Le même mécanisme simplifie les tests. Un test unitaire passe la fabrique de son choix, vérifie la méthode, l'URI, l'en-tête Content-Type et le contenu du corps, sans réseau et sans dépendre de la bibliothèque utilisée en production. La formation PHP orienté objet : concevoir des applications structurées travaille ce type de découpage par interfaces, et Symfony 7 : initiation au framework PHP professionnel montre comment le conteneur s'en charge.
Fabrique abstraite ou Fabrique ?
Les deux patterns sont voisins dans le tableau et souvent confondus. Quatre repères permettent de les distinguer dans le code.
- Nombre de produits. La Fabrique crée un seul type d'objet. La Fabrique abstraite en crée plusieurs qui forment une famille cohérente.
- Mécanisme de variation. La Fabrique repose sur l'héritage : une sous-classe redéfinit une méthode de création. La Fabrique abstraite repose sur la composition : le client reçoit un objet fabrique, et on le remplace pour changer de famille.
- Lien entre les deux. Chaque méthode d'une Fabrique abstraite est elle-même une méthode de fabrique.
createRequest()etcreateStream()en sont deux, regroupées dans une même classe par Nyholm comme par Guzzle. - Question à se poser. Si le choix porte sur un objet isolé, la Fabrique suffit. Si plusieurs objets doivent provenir du même fournisseur pour rester compatibles, la Fabrique abstraite devient pertinente.
Les pièges
L'interface fourre-tout
Une Fabrique abstraite grossit à chaque nouveau produit, et un client qui ne crée que des requêtes finit par dépendre d'une interface à six méthodes. PSR-17 évite ce défaut en découpant la famille en six interfaces distinctes, ce qui permet à ClientAlertes de ne demander que les deux dont il a besoin. C'est l'application du principe de ségrégation des interfaces, présenté dans l'article sur les principes SOLID.
Ajouter un produit casse toutes les fabriques
Le Gang of Four signale lui-même cette limite : ajouter un nouveau type de produit oblige à modifier l'interface et toutes ses implémentations. Avec trois fabriques concrètes, une nouvelle méthode signifie trois classes à compléter avant que le code compile à nouveau. Une famille dont la liste de produits bouge souvent se prête mal au pattern.
Mélanger deux familles sans le voir
Rien n'empêche de passer la fabrique de requêtes de Nyholm et la fabrique de flux de Guzzle au même client. Avec PSR-7, le résultat reste fonctionnel puisque chaque bibliothèque accepte n'importe quel flux conforme à l'interface. Dans une famille non standardisée, le même mélange produit des objets qui ne savent pas travailler ensemble. Passer la même instance pour les deux paramètres, comme dans l'exemple d'assemblage, élimine le risque à la source.
Étendre une fabrique déclarée final
La classe HttpFactory de Guzzle est déclarée final : impossible d'en hériter pour modifier un comportement. Le besoin d'ajouter un en-tête par défaut ou de journaliser chaque création passe donc par une classe qui implémente la même interface et délègue à la fabrique d'origine, soit un Décorateur, dans l'esprit du principe composition plutôt qu'héritage.
<?php
declare(strict_types=1);
namespace App\Http;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
final readonly class RequestFactoryAvecUserAgent implements RequestFactoryInterface
{
public function __construct(
private RequestFactoryInterface $interne,
private string $userAgent,
) {
}
public function createRequest(string $method, $uri): RequestInterface
{
return $this->interne
->createRequest($method, $uri)
->withHeader('User-Agent', $this->userAgent);
}
}
Le décorateur implémente RequestFactoryInterface, reçoit la vraie fabrique dans son constructeur et ajoute l'en-tête à chaque requête produite. Pour le reste du code, il s'agit d'une fabrique de requêtes comme une autre : ClientAlertes l'accepte sans modification, et l'en-tête apparaît dans toutes ses requêtes. La signature de createRequest() reprend exactement celle de l'interface PSR-17, paramètre $uri non typé compris, puisque l'interface accepte une chaîne ou un objet UriInterface.
Créer la fabrique avant la deuxième famille
Une application qui n'utilisera jamais qu'une seule implémentation n'a pas besoin d'une Fabrique abstraite maison. Elle ajoute une interface, une classe et une configuration sans bénéfice mesurable. YAGNI s'applique ici pleinement. Le cas PSR-17 fait exception, parce que l'interface existe déjà et ne coûte rien à adopter.