The Daily Commit · Édition de rubrique La une PHP AI Dev EN DE FR ES

The Php Times

L’Éditorial — Écosystème

PHP 8.5 te donne deux parseurs d'URL, et c'est le second qui rend le tout sûr


PHP 8.5 livre Uri\Rfc3986\Uri et Uri\WhatWg\Url, et la critique évidente, c'est qu'une seule classe propre aurait été plus simple.

29 septembre 2026 Un éditorial de Kai

Kai soutient que cette séparation est la vraie correction : la plupart des bugs d'URL viennent de deux composants qui lisent la même chaîne différemment, et désormais le langage t'oblige à choisir la lecture à laquelle tu fais confiance. Voici pourquoi, et où ça va faire mal.

Un collègue m'a envoyé un diff la semaine dernière, avec pour seul commentaire : « pourquoi il y en a DEUX maintenant ? » Le diff remplaçait un appel à parse_url() dans notre validateur de webhooks par Uri\WhatWg\Url, et dans la même PR, un autre fichier utilisait Uri\Rfc3986\Uri pour nos identifiants de services internes. Il y a vu un PHP 8.5 incapable de se décider. Moi, j'y vois la chose la plus honnête arrivée aux URL en PHP depuis l'apparition de parse_url() à l'époque de PHP 4. Avoir deux classes, c'est la correction, pas un vestige.

Petit rappel si tu n'as pas encore touché à 8.5. La nouvelle extension URI est intégrée, donc rien à installer via Composer. Uri\Rfc3986\Uri repose sur la bibliothèque uriparser et suit la RFC 3986, la grammaire générale des identifiants, qui couvre les URN comme urn:uuid:..., les schémas personnalisés et les références relatives comme /path/foo. Uri\WhatWg\Url tourne sur Lexbor, le moteur que PHP 8.4 a déjà apporté pour l'API DOM, et lit les URL exactement comme un navigateur. Elle refuse une référence relative si tu ne lui donnes pas de base, et elle sait que münchen.example et sa forme Punycode désignent le même hôte, d'où getUnicodeHost(), getAsciiHost() et toAsciiString(). Les deux classes sont immuables, les deux proposent des méthodes with*() à la manière de PSR-7, et les deux ont un equals() auquel tu peux passer UriComparisonMode::IncludeFragment si le fragment doit compter.

Si cette séparation me tient à cœur, c'est à cause d'un type de bug précis qui m'a coûté quelques nuits. Ton validateur lit une URL et décide que l'hôte est autorisé. Puis ton client HTTP, ou curl en dessous, ou un proxy plus loin, lit les mêmes octets et se connecte ailleurs. Aucun composant n'a tort à lui seul. Ils ne sont simplement pas d'accord sur la chaîne, et c'est dans ce désaccord que se niche une SSRF. La documentation PHP le dit d'ailleurs : faire passer la validation et la récupération de ressources par des parseurs différents peut ouvrir des failles de sécurité. parse_url() n'a jamais cherché à suivre la moindre norme, donc il pouvait contredire à peu près tout le monde, et pendant des années on a fait semblant que ce tableau de clés présentes ou non méritait le nom de parsing.

Avec une seule classe « Uri », l'équipe des internals aurait dû désigner un gagnant, et la norme perdante aurait été tordue pour rentrer dans le moule de l'autre. Tu te souviens de la vieille blague selon laquelle PHP a trois façons de tout faire ? Ici, c'est l'inverse : deux façons parce que le monde a vraiment deux définitions, et le langage a décidé d'arrêter de le masquer. Si la chaîne part vers un client HTTP, vient d'un navigateur ou sera renvoyée dans une redirection, prends WHATWG, parce que tout le reste du circuit la lit déjà comme ça. Si c'est un identifiant inventé par tes propres systèmes, comme une URN, une adresse de file ou un deep link my-app://, prends RFC 3986, car un modèle de navigateur n'a pas son mot à dire là-dessus.

Le meilleur argument contre moi mérite qu'on l'écoute sérieusement. La plupart d'entre nous n'ont jamais voulu devenir juristes des normes. Un junior qui veut juste récupérer l'hôte d'un lien doit maintenant comprendre pourquoi deux specs existent avant d'écrire la moindre ligne, et la page qui l'explique est plus longue que ne l'a jamais été la doc de parse_url(). Les implémentations PSR-7 gèrent des URI immuables depuis des années, et des bibliothèques comme league/uri couvrent les cas limites depuis une décennie, alors tu peux légitimement te demander ce que le cœur du langage apporte, à part un second vocabulaire. Et puis parse_url() est toujours là, toujours rapide, et toujours parfait pour extraire le chemin d'une URL que tu as construite toi-même trente lignes plus haut.

J'accepte presque tout ça, et j'arrive quand même à la même conclusion, pour deux raisons. La première, c'est qu'une bibliothèque userland ne peut pas mettre toute ta stack d'accord. Une classe du cœur, sur laquelle les clients HTTP des frameworks, les validateurs et les adaptateurs PSR-7 peuvent tous s'appuyer, le peut, avec le temps, et un parseur partagé, c'est justement ce qui referme la brèche du désaccord. La seconde, c'est la courbe d'apprentissage. Savoir si ta chaîne est un lien web ou un identifiant, c'est la vraie question, et parse_url() te laissait l'esquiver jusqu'au jour où un rapport de bug t'obligeait à y répondre. Sans compter que Url::parse(), qui renvoie null et remplit un tableau $errors d'objets UrlValidationError, est franchement plus agréable pour des saisies de formulaire qu'un try/catch autour de chaque champ.

Voici donc ma règle pour notre codebase, et n'hésite pas à la piquer. Partout où une URL franchit une frontière de confiance (saisie utilisateur, webhooks, redirections, tout ce qui finit dans une requête sortante), parse_url() est interdit, et le parseur qui valide doit être la même classe que celle utilisée par le code qui fait la requête. Ailleurs, migre quand tu passes de toute façon dans le fichier. Réécrire trois cents appels inoffensifs en un seul sprint, c'est transformer une migration raisonnable en incident dont tout le monde retrouvera la trace dans le git blame.

Ce que je n'ai pas encore résolu, c'est la jointure entre les deux. Ton objet requête PSR-7 contient une URI, ton client sortant veut sans doute la sémantique WHATWG, et ton routeur raisonne probablement en termes de RFC 3986. À quel endroit de ton application fais-tu la conversion, et quelle classe mets-tu dans tes objets métier : un seul type partout, ou les deux, avec une frontière explicite ? J'aimerais vraiment savoir où tes équipes tracent cette ligne.

Évaluer cet article : 0

Tribune des lecteurs

Pas encore de contributions — lance le débat.

← Écosystème — Page B1

"All the Code That's Fit to Ship" · The Daily Commit · Édition écran · Mentions légales · Politique de confidentialité