PHP se met au fonctionnel, le pipe arrive !

Posted on Jul 10, 2025

Snippet of code with the new pipe operator in PHP 8.5

J’en parle partout : j’aime de plus en plus programmer avec des concepts fonctionnels. Même en PHP. Ce n’est pas pour rien que j’ai créé Variant et le projet Hopr, avec son Result tout droit inspiré de Gleam.

PHP n’est pas vraiment connu pour sa pureté fonctionnelle, c’est surtout un langage multi-paradigme, principalement utilisé comme langage orienté objet. Mais depuis quelques versions, PHP nous donne des outils pour écrire du code plus expressif. Et l’opérateur pipe qui arrive en PHP 8.5 ? C’est un vrai changement.

On le retrouve dans beaucoup de langages fonctionnels, pour n’en citer que quelques-uns :

  • Gleam, Roc, Elixir, Elm, F#, OCaml avec |>
  • Haskell avec & (sérieusement…)
  • ReScript avec ->
  • Bash avec | (eh oui… mais Bash n’est pas fonctionnel)

Disclaimer : Je ne vais pas typer mon code, juste par praticité de rédaction, mais cela fonctionne avec des types, et c’est même ce qu’il faut faire !


Un exemple pour comprendre

Prenons un cas concret : j’ai des résultats de recherche d’articles de blog, et je veux récupérer uniquement les titres de ceux qui parlent de “Type System”.

$search_results = [
    [
        "position" => 1,
        "title" => "Hippo : des pages réactives en pur PHP",
        "website" => "https://courteau.dev/posts/hippo-framework-php-websocket-fullstack/",
        "category" => "Web Development",
        "tags" => ["PHP", "WebSocket", "Reactivity", "Proof-of-Concept (POC)", "Async"],
    ],
    [
        "position" => 2,
        "title" => "Gérer les erreurs en PHP",
        "website" => "https://courteau.dev/posts/handling-errors-in-php/",
        "category" => "Software Engineering",
        "tags" => ["PHP", "Error Handling", "Functional Programming", "Type System"],
    ],
    // ... le reste
];

L’objectif : filtrer les éléments qui contiennent “Type System” dans leurs tags, puis extraire uniquement les titres.


Version 1 : L’approche classique

$filtered = [];
foreach ($search_results as $search_result) {
    if (in_array("Type System", $search_result["tags"])) {
        $filtered[] = $search_result["title"];
    }
}

Ça marche. C’est du PHP standard. Mais on mélange la logique de filtrage avec celle de transformation, et on manipule un état mutable. La lecture est relativement claire sur ces cinq lignes de code, mais dans un spaghetti de 1200 lignes avec des mutations partout, c’est rapidement un enfer.


Version 2 : Les fonctions natives

On peut passer par des fonctions map / filter / reduce pour limiter les mutations, ou du moins faire des mutations par étapes. C’est plus simple à déboguer.

$filtered = array_filter($search_results, fn($search) => in_array("Type System", $search["tags"]));
$filtered = array_map(fn($search) => $search['title'], $filtered);

Les fonctions fléchées de PHP 7.4 nous permettent de séparer les opérations. On filtre d’abord, puis on transforme. C’est déjà plus lisible. On ne mélange plus les responsabilités du code, ou disons, on les mélange moins.


L’opérateur pipe rapidement

L’opérateur pipe |> prend la valeur à gauche et la passe comme premier argument à la fonction à droite. C’est un concept emprunté aux langages fonctionnels comme F#, Elixir ou Gleam.

Au lieu d’écrire map(filter($data, $predicate), $transform), on écrit $data |> filter($predicate) |> map($transform). C’est plus naturel à lire.


Version 3 : L’opérateur pipe arrive

L’opérateur pipe |> permet de chaîner les opérations de gauche à droite, comme dans les langages fonctionnels. Au lieu d’imbriquer les fonctions ou d’utiliser des variables intermédiaires, on peut lire le code comme une séquence d’étapes.

$filtered = $search_results
    |> fn($x) => array_filter($x, fn($search) => in_array("Type System", $search["tags"]))
    |> fn($x) => array_map(fn($search) => $search['title'], $x);

Le code se lit maintenant : « Prends les résultats, filtre-les, puis récupère les titres. » C’est plus expressif.

PHP 8.5 permettra de passer la valeur de gauche dans une fonction à un seul argument. C’est pour cela que je crée des closures qui exécutent array_filter et array_map.

Une RFC ajoutant la possibilité de choisir l’argument qui sera remplacé, et donc de fournir des fonctions à plusieurs arguments, est en cours.


Version 4 : Curry et application partielle

// On va définir des fonctions utilitaires qui transforment une fonction à 2 arguments en deux fonctions à 1 argument
function filter($fn) {
    return fn($arr) => array_filter($arr, $fn);
}

function map($fn) {
    return fn($arr) => array_map($fn, $arr);
}

$filtered = $search_results
    |> filter(fn($search) => in_array("Type System", $search["tags"]))(...)
    |> map(fn($search) => $search['title'])(...);

On crée nos propres fonctions d’ordre supérieur. L’application partielle avec (...) nous permet de créer des fonctions spécialisées.

C’est mieux, mais pas super clair pour un néophyte, et j’ai potentiellement beaucoup de duplication de code.


Version 5 : Composition et réutilisabilité

$isTypeSystem = fn($arr) => in_array("Type System", $arr["tags"]);
$onlyTakeTitle = fn($arr) => $arr['title'];

$filterTS = filter($isTypeSystem);
$mapTitle = map($onlyTakeTitle);

$filtered = $search_results
    |> $filterTS(...)
    |> $mapTitle(...);

On a maintenant des fonctions réutilisables et composables. $filterTS peut servir ailleurs, $mapTitle aussi. Tout est facilement testable. J’aime beaucoup le style de cette approche, même s’il est difficile de nier sa complexité pour un non-initié. C’est un code qui parlera mieux aux développeurs JS/TS.


Pourquoi cette approche ?

✅ La lisibilité

Le code est expressif, on comprend le flux de données. Chaque partie a le moins de responsabilité possible.

✅ La réutilisabilité

Les fonctions peuvent être utilisées dans d’autres contextes.

✅ La testabilité

Chaque fonction fait une seule chose, donc elle est facile à tester.

✅ L’immutabilité

On ne modifie rien, on crée du nouveau. Moins de bogues liés aux mutations.


Une programmation déclarative

On se concentre sur le quoi (ce que l’on veut accomplir) plutôt que sur le comment (les étapes précises pour y arriver).

Au lieu d’écrire une boucle foreach avec une condition if à l’intérieur pour construire manuellement un nouveau tableau (le “comment”), on déclare une séquence de transformations.

La ligne suivante se lit comme une phrase en langage naturel : $search_results |> $filterTS(...) |> $mapTitle(...) « Prends les résultats de recherche, puis filtre-les pour ne garder que ceux avec le tag “Type System”, puis extrais uniquement les titres. »

On décrit le résultat final souhaité, et les détails d’implémentation (les boucles, les vérifications) sont abstraits et encapsulés dans les fonctions filter et map. Cela réduit la charge cognitive du développeur (en tout cas chez moi) et rend le code beaucoup moins sujet aux erreurs.


Dans les chiffres

Bon, c’est bien beau, mais quel est le gain ? Seulement celui du style et de la maintenabilité !

Le souci, c’est qu’en séparant chaque tâche, on se retrouve à boucler N fois la liste (2 ici), pour filtrer et mapper, contre une seule fois avec la version en boucle.

Ma recommandation serait donc de faire un arbitrage :

  1. Code sensible à la performance : boucle procédurale.
  2. Code sensible à la maintenance et à la lisibilité : pipe.

Dans tous les cas, vous serez toujours entre ces deux extrêmes. Il faut donc benchmarker et s’adapter.

J’ai tendance à penser que si on cherche de la performance brute en PHP, on n’a pas choisi le bon langage — mais ce n’est que mon avis, très discutable. Je choisis donc toujours la solution qui permet la maintenance la plus simple.

Résultats pour un micro-benchmark en local : (durée en ms pour 10k itérations, sur un array de 5 éléments)

Array
(
    [foreach] => Array
        (
            [time] => 39.50309753418
        )

    [filter_map] => Array
        (
            [time] => 92.856168746948
        )

    [pipe] => Array
        (
            [time] => 126.72114372253
        )

    [curry_partial] => Array
        (
            [time] => 125.59509277344
        )

    [curry_partial2] => Array
        (
            [time] => 88.146924972534
        )
)

Une longue attente, et de l’impatience

Le plus compliqué avec cette approche, c’est de revenir vers du PHP procédural après. Une fois qu’on a goûté à cette expressivité, c’est difficile de s’en passer.

Alors oui, c’était possible de faire la même chose ou presque avec des classes, mais on n’a pas tout le temps besoin de POO. Les fonctions ne sont pas des first-class citizens depuis très longtemps en PHP, et cet operator pipe est un très bon signal !

Évidemment, je ne dis pas qu’il faut tout réécrire. Mais pour du code neuf, cette approche apporte vraiment quelque chose. Et avec l’opérateur pipe de PHP 8.5, on va avoir des outils encore plus puissants.

C’est plaisant de voir PHP évoluer dans cette direction grâce à Larry Garfield (https://github.com/crell).