jobsphp

Standards PSR : pourquoi l'interopérabilité sauve le PHP

Open Source & Outils. Standards PSR : pourquoi l'interopérabilité sauve le PHP

composer require — la commande la plus exécutée de l'écosystème PHP. Derrière ces caractères, un contrat silencieux: chaque paquet installé doit charger ses classes sans collision, journaliser ses…

Standards PSR: pourquoi l'interopérabilité sauve le PHP

composer require — la commande la plus exécutée de l'écosystème PHP. Derrière ces caractères, un contrat silencieux: chaque paquet installé doit charger ses classes sans collision, journaliser ses événements via une interface unique, échanger des requêtes HTTP par un client interchangeable. Ce contrat est le travail du PHP-FIG et de ses PSR. Sans ces standards, l'interopérabilité des bibliothèques PHP n'existe pas en pratique.

Le terme « standard » prête à confusion. Une PSR n'est pas une fonctionnalité du langage. C'est une spécification publiée par le PHP-FIG, distribuée via des paquets Composer, qui définit un contrat entre bibliothèques. Le respect de ce contrat est volontaire. Son application est sociale: adopter les PSR, c'est accepter que son code dialogue avec celui des autres sans couche d'adaptation propriétaire.

L'autochargement moderne: au-delà de la dépréciation de PSR-0

L'autochargement est le premier contrat inter-bibliothèques. Avant PSR-4, PSR-0 imposait une cartographie stricte entre namespaces et arborescence de fichiers. Le 21 octobre 2014, PSR-0 a été marqué déprécié par le PHP-FIG. Présenter PSR-0 comme le standard d'autoloading de référence pour un nouveau projet est une erreur factuelle — la convention active est PSR-4, sans ambiguïté depuis une décennie.

Le mécanisme de PSR-4 associe un préfixe d'espace de noms à un répertoire. Le nom de classe terminal correspond à un fichier .php. La résolution est déterministe: si le préfixe Acme\Foo\ est mappé sur src/, la classe Acme\Foo\Bar charge src/Bar.php. Un autoloader PSR-4 ne doit ni lever d'exception ni émettre d'erreur. Un échec de chargement reste silencieux et laisse l'autoloader suivant tenter sa chance. Cette contrainte évite les cascades d'exceptions en environnement de production.

Composer matérialise ce contrat. Le champ autoload de composer.json accepte une section psr-4 qui déclare les préfixes et les chemins. Forme typique: "autoload": { "psr-4": { "Acme\\": "src/" } }. Après composer install ou composer update, Composer génère vendor/autoload.php. Un require 'vendor/autoload.php'; enregistre l'autoloader pour l'ensemble du projet. Toute modification du champ autoload impose composer dump-autoload pour régénérer le registre — sinon l'ancien cache continue de servir.

Points opérationnels à maîtriser:

  • Les chemins sont relatifs à la racine du paquet déclarant.
  • Plusieurs préfixes peuvent coexister, pointant vers des répertoires distincts.
  • Un même répertoire peut héberger plusieurs préfixes, configuration courante dans les mono-repos ou les bundles composites.
  • Les classes non-PSR-4 peuvent être chargées via classmap (scan explicite) ou files (chargement inconditionnel), mais ces mécanismes sortent du périmètre de la PSR-4.
PSR-4 fixe la cartographie entre namespace et chemin de fichier. L'algorithme de résolution appartient à Composer, Symfony, Laminas, ou toute autre implémentation conforme.

L'erreur fréquente consiste à mélanger PSR-0 et PSR-4 dans un même composer.json pour absorber une dépendance legacy. La coexistence est techniquement possible, mais elle fragmente la base d'autoload et complique le refactoring. Une migration PSR-0 → PSR-4 d'une bibliothèque interne se traite en une passe: déplacement des fichiers, ajustement du champ autoload, suppression des underscores dans les namespaces de l'ancienne norme.

La standardisation des interfaces: PSR-3, PSR-7, PSR-15 et PSR-18

Une bibliothèque qui écrit des logs ne devrait pas imposer Monolog, Apache log4php, ou un logger maison. PSR-3 résout ce couplage via Psr\Log\LoggerInterface. L'interface expose huit méthodes alignées sur les niveaux de sévérité de la RFC 5424:

1. debug

2. info

3. notice

4. warning

5. error

6. critical

7. alert

8. emergency

Une bibliothèque qui dépend de LoggerInterface reçoit un logger par injection, ne le crée pas, ne le configure pas. Elle peut être branchée sur Monolog, Symfony Logger, NullLogger, ou toute autre implémentation conforme, sans modification. Cette inversion de dépendance transfère la responsabilité de la journalisation au projet hôte, pas à la bibliothèque.

PSR-7 applique la même logique au protocole HTTP. Les interfaces Psr\Http\Message\RequestInterface, ResponseInterface, StreamInterface, UriInterface normalisent la représentation des messages. La contrainte centrale est l'immuabilité. Une méthode qui modifie potentiellement l'état — withHeader(), withBody(), withMethod() — retourne une nouvelle instance. Le message d'origine n'est jamais altéré. Cette propriété permet à une chaîne de transformations de composer plusieurs opérations sans effet de bord.

Les références normatives sont explicites: RFC 7230 et 7231 pour la syntaxe des messages HTTP, RFC 3986 pour la syntaxe des URI. PSR-7 ne réinvente pas HTTP. Il le rend adressable en PHP, dans un typage strict défini par la spécification.

PSR-18 complète la chaîne côté client. L'interface Psr\Http\Client\ClientInterface définit une méthode unique: sendRequest(RequestInterface $request): ResponseInterface. Un client conforme reçoit une requête PSR-7 et renvoie une réponse PSR-7. Le point critique de la spécification: les réponses HTTP 4xx et 5xx ne doivent pas être traitées comme des exceptions par défaut. Une réponse, même 500, reste un résultat valide du protocole. La spécification isole deux types d'exceptions:

  • RequestExceptionInterface: la requête est mal formée ou invalide avant envoi.
  • NetworkExceptionInterface: la requête n'a pas pu atteindre le serveur (timeout, DNS, refus de connexion).

Un client conforme qui lèverait une exception sur tout code 4xx violerait PSR-18. Le test de conformité vérifie précisément ce comportement — une réponse 404 doit être retournée normalement, pas jetée comme exception.

Reste la question des middlewares, souvent confondue avec celle des clients. L'adoption combinée de PSR-7 et de PSR-18 rend les clients HTTP interchangeables entre implémentations: Guzzle 7, Symfony HttpClient, un wrapper PSR-18 sur mesure — tous substituables tant qu'ils exposent ClientInterface. Le code applicatif injecte l'interface, jamais une classe concrète. Pour les middlewares, l'interopérabilité relève d'un contrat séparé: PSR-15. Cette norme définit Psr\Http\Server\MiddlewareInterface et Psr\Http\Server\RequestHandlerInterface, qui s'appuient sur les messages PSR-7 mais ajoutent leur propre couche d'abstraction. La signature canonique d'un middleware est process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface — le middleware traite la requête, la passe éventuellement au handler suivant, puis renvoie la réponse finale. Deux middlewares conformes à PSR-7 mais non alignés sur PSR-15 ne sont pas interchangeables: leur signature d'appel diffère, leur contrat de délégation diffère, leur chaînage diffère. Inversement, une pile PSR-15 s'enfile dans n'importe quel framework exposant un dispatcher conforme — Mezzio, Slim 4, Laravel via adaptateur — sans réécriture.

PSRInterface centraleMéthode ou propriété cléRéférence normative
PSR-3Psr\Log\LoggerInterface8 niveaux de logRFC 5424
PSR-7Psr\Http\Message\RequestInterface, ResponseInterfaceImmuabilité, with*() retourne un cloneRFC 7230, RFC 7231, RFC 3986
PSR-15Psr\Http\Server\MiddlewareInterface, RequestHandlerInterfaceprocess(ServerRequestInterface, RequestHandlerInterface): ResponseInterfaceRFC 7230
PSR-18Psr\Http\Client\ClientInterfacesendRequest(RequestInterface): ResponseInterfaceRFC 7230

L'injection de dépendances et le rôle du conteneur PSR-11

PSR-11 standardise l'accès aux conteneurs d'injection de dépendances via Psr\Container\ContainerInterface. Deux méthodes seulement: get($id) et has($id). L'interface est volontairement minimale — toute logique de construction, de scoping, d'autowiring, ou de compilation reste à la charge de l'implémentation (Symfony DependencyInjection, PHP-DI, Pimple, Laravel Container).

La spécification introduit une restriction claire: passer un conteneur à un objet métier pour qu'il récupère lui-même ses dépendances revient à employer le patron Service Locator. La documentation du PHP-FIG le déconseille explicitement. Le raisonnement est technique. Le Service Locator masque le graphe de dépendances d'une classe. Un test unitaire qui injecte un mock devient laborieux: il faut amorcer le conteneur au lieu de passer une dépendance au constructeur. Une refactorisation qui change le type d'une dépendance nécessite de fouiller le conteneur au lieu de modifier une signature de méthode.

L'usage sain de PSR-11 suit trois règles opérationnelles:

  • Le conteneur assemble le graphe au démarrage de l'application, puis disparaît du code applicatif.
  • Les objets métier reçoivent leurs dépendances par constructeur, jamais via un appel à $container->get().
  • Les contrôleurs, commandes, ou handlers déclarent leurs dépendances dans leur signature. Le conteneur les résout via l'autowiring.
Un conteneur PSR-11 est un assembleur de graphe, pas un registre d'objets vivants à consulter à l'exécution.

Les frameworks PHP matures utilisent PSR-11 en interne sans l'exposer dans le code applicatif. Symfony compile le graphe dans un fichier de cache compilé, lu en production pour éviter la résolution runtime. Laravel expose le conteneur via la facade app() — un anti-pattern borderline que les équipes apprennent à éviter en code de domaine. La règle reste: pas de référence au conteneur dans une classe métier.

Style et rigueur: les limites de PSR-12 et des bonnes pratiques

PSR-12 est une norme de style étendue, héritière de PSR-2. Elle régit l'indentation (4 espaces), les sauts de ligne entre use, la position des accolades, l'ordre des déclarations, la longueur des lignes. Ce n'est pas une norme sémantique. Elle ne rend pas declare(strict_types=1) obligatoire. Le PHP-FIG a explicitement considéré cette question hors du périmètre d'un guide de style. Activer le typage strict est un choix d'équipe, documenté dans le README ou la convention de projet, jamais déduit de la conformité à PSR-12.

La confusion est répandue. Un code PSR-12 conforme peut être typé dynamiquement: il respecte les espaces, les sauts de ligne, l'ordre des use, mais reste vulnérable aux coercitions implicites de PHP. Inversement, un fichier typé strictement (declare(strict_types=1); en tête) peut violer PSR-12 sans qu'aucun outil ne s'en offusque — la norme ne couvre pas le typage.

Trois familles d'outillage ferment la boucle sur la qualité du code:

  • Style: PHP_CodeSniffer pour la détection des violations, PHP-CS-Fixer pour la correction automatique.
  • Analyse statique et typage: PHPStan et Psalm, configurables par niveaux (0 à max), avec vérification de null safety et des types d'union.
  • Tests: PHPUnit ou Pest pour la couverture comportementale du contrat métier.

Un pipeline CI qui combine les trois familles couvre le périmètre normatif (PSR-12) et le périmètre sémantique (typage strict, absence de null non documenté, types de retour cohérents). La conformité stylistique seule est cosmétique. La conformité sémantique seule est illisible sur la durée. Les deux ensembles constituent l'outillage minimum d'un projet PHP maintenu sur plusieurs années.

DimensionOutilPérimètre vérifié
StylePHP_CodeSniffer, PHP-CS-FixerPSR-12
SémantiquePHPStan, Psalmstrict_types, null safety, types
ComportementPHPUnit, PestContrat métier

L'outillage ne remplace pas la revue humaine. Un code PSR-12 + strict_types + Psalm niveau max reste opaque pour une équipe qui découvre les types d'union ou les génériques introduits par PHPStan. La norme ne couvre pas la diffusion des conventions au sein de l'équipe ni la formation aux nouveautés du typage PHP.

La réalité du terrain: pourquoi la conformité n'est pas une interchangeabilité totale

La conformité à une interface PSR garantit un contrat syntaxique. Elle ne garantit pas la compatibilité comportementale. Cinq vecteurs de divergence persistent.

Versions de PHP. Une bibliothèque peut exiger PHP 8.2+, une autre 7.4. La conformité PSR-4 ne change rien: les deux autoloaders fonctionnent, mais l'imbrication des dépendances peut forcer une mise à niveau de la stack entière, avec rupture de rétrocompatibilité sur les APIs dépréciées ou supprimées entre versions mineures.

Extensions natives. PSR-7 utilise des flux PHP (StreamInterface) pour le body des requêtes. Une implémentation peut s'appuyer sur ext-curl, une autre sur ext-sockets ou une couche interne. Si l'environnement de production interdit l'une de ces extensions, la conformité PSR-18 ne suffit pas — il faut vérifier la chaîne de dépendances natives de chaque implémentation.

Résolution Composer. composer update écrit les versions exactes résolues dans composer.lock. Un composer install ultérieur réinstalle ces versions, maintenant la cohérence entre environnements. Mais deux paquets conformes au même PSR peuvent tirer des versions incompatibles d'une dépendance transitive. Le résolveur arbitre, parfois en forçant une mise à niveau imprévue, parfois en échec avec un message Conflict qui bloque l'installation.

Comportements hors spécification. PSR-18 impose de renvoyer une réponse PSR-7. Elle ne dit rien du timeout par défaut, du nombre de retries, du comportement face à un certificat SSL expiré ou à un proxy d'entreprise. Deux clients conformes peuvent diverger radicalement sur ces paramètres. Le code applicatif qui suppose un timeout de 30 secondes se trouve désarmé face à un client configuré à 5 secondes par défaut.

Câblage applicatif. Une application Symfony qui injecte un service dans un contrôleur via le conteneur n'est pas automatiquement compatible avec une application Laravel qui fait la même chose. Les déclarations diffèrent (services.yaml contre Service Provider), les scopes diffèrent (singleton, request-scoped, transient), les hooks de cycle de vie diffèrent (compiler pass contre méthode boot).

La conformité PSR est une condition nécessaire à l'interchangeabilité, jamais une condition suffisante.

Une architecture qui s'appuie uniquement sur les PSR pour garantir l'interchangeabilité s'expose à des régressions silencieuses au changement de version d'une dépendance transitive, d'une extension native, ou du moteur PHP lui-même. Le travail d'intégration ne s'arrête pas à composer require. Il commence après.

Verdict: les PSR sauvent le PHP de l'enfermement propriétaire et standardisent les contrats critiques — autoload, logs, HTTP, middlewares, conteneur, style. À utiliser en production sans réserve pour les couches basses de l'architecture. À compléter par une vérification systématique des versions PHP, des extensions natives, et de la résolution Composer pour les couches métier. La conformité d'interface ouvre la porte. L'audit de compatibilité ferme le contrat.

Questions fréquentes

Pourquoi PSR-0 est-il déconseillé pour un nouveau projet PHP ?
PSR-0 a été marqué comme déprécié par le PHP-FIG le 21 octobre 2014. La convention active et recommandée depuis une décennie est PSR-4, qui offre une gestion plus souple et efficace de l'autochargement.
Quelle est la règle d'or pour utiliser un conteneur PSR-11 ?
Le conteneur doit servir uniquement à assembler le graphe de dépendances au démarrage de l'application. Il ne doit jamais être injecté dans les objets métier pour récupérer des services, car cela constitue un anti-pattern de type Service Locator.
Un code conforme à PSR-12 est-il obligatoirement typé strictement ?
Non, PSR-12 est une norme de style qui régit uniquement la présentation du code, comme l'indentation ou l'ordre des déclarations. L'activation du typage strict via declare(strict_types=1) reste un choix de projet indépendant de la conformité stylistique.
Pourquoi ne faut-il pas lever d'exception sur une réponse HTTP 4xx avec PSR-18 ?
La spécification PSR-18 considère qu'une réponse HTTP 4xx ou 5xx est un résultat valide du protocole. Les exceptions ne doivent être réservées qu'aux cas où la requête est mal formée ou n'a pas pu atteindre le serveur.
Quelle est la différence entre PSR-7 et PSR-15 ?
PSR-7 normalise la représentation des messages HTTP (requêtes et réponses) via des interfaces immuables. PSR-15 s'appuie sur ces messages pour définir un contrat standardisé de middleware et de gestionnaire de requêtes.