El lado derecho del nuevo operador |> de PHP 8.5 tiene que ser un callable, no una llamada a función. Todo lo interesante de la funcionalidad se esconde en ese requisito. El operador en sí cabría en un pósit: evalúa la expresión de su izquierda y pasa el resultado como primer argumento a lo que esté a su derecha. Así que escribes $input |> trim(...), apoyándote en la sintaxis de callables de primera clase que nos llegó allá por 8.1. Pero en cuanto un paso necesita un segundo argumento, digamos un str_replace con búsqueda y reemplazo, tienes que envolverlo: (fn($s) => str_replace(' ', '-', $s)), y los paréntesis exteriores no son decoración. Quítalos y la arrow function captura con avidez el resto de la cadena y el parser te abandona a tu suerte.

sanitize.php
<?php
// PHP 8.5: each step takes a value, returns a value
function sanitizeComment(string $input): string
{
    return $input
        |> trim(...)
        |> strip_tags(...)
        |> (fn($s) => htmlspecialchars($s, ENT_QUOTES, 'UTF-8'));
}

Ese envoltorio es la queja que más oigo sobre la implementación actual, y sí, está previsto que PHP 8.6 traiga aplicación parcial de funciones para que puedas prerrellenar argumentos y librarte de la mayoría de estas closures. Espero que se tome su tiempo. La verbosidad está haciendo un trabajo pedagógico silencioso. Cada vez que tecleas fn($s) =>, el lenguaje te hace una pregunta pequeña pero afilada: ¿este paso es de verdad un apaño anónimo, o es algo con nombre que tu base de código va a querer otra vez el martes que viene?

Porque cuando los pasos sí tienen nombre, el operador de repente se gana el sueldo. Los métodos estáticos se encadenan directamente con la sintaxis de callables, así que $raw |> Slug::normalize(...) |> Slug::truncate(...) se lee de arriba abajo sin ceremonia alguna. Los métodos de instancia no gozan de ese lujo, todavía tienes que envolver $formatter->format() en una closure, lo cual es una verruga genuina, pero también significa que el código con pipes tiende hacia funciones pequeñas sin estado y con una entrada obvia. Esa deriva es gratis en tiempo de ejecución: las cadenas de pipes compilan a opcodes más o menos equivalentes a las llamadas anidadas que habrías escrito de todos modos, así que no hay factura de rendimiento por trocear tu pipeline en una docena de piezas pequeñas con nombre.

Ahora la parte honesta. El argumento más fuerte contra molestarse con |> es que PHP ya tenía una respuesta perfectamente servible: variables intermedias. $trimmed, $slugified, $lowercased. Esos nombres documentan la intención, aparecen en tu depurador, puedes hacer var_dump de cualquiera de ellos sin ceremonia. Una cadena de pipes no te da nada de eso, y añade una trampa que el estilo antiguo nunca tuvo: un callable void evalúa a null, así que si metes un var_dump en mitad de una cadena para echar un vistazo rápido, el paso siguiente recibe null en lugar de tu valor y el pipeline envenena en silencio todo lo que viene después. Sin excepción, sin aviso, solo una salida incorrecta tres pasos más tarde. Súmale la realidad del despliegue, esto es sintaxis nueva que no va a parsear en 8.4 ni en nada anterior, así que toda tu flota y tus runners de CI tienen que estar primero en 8.5, y el escepticismo parece bastante razonable.

Aun así me quedo con usarlo, y la trampa del var_dump es parte del motivo. Una cadena de pipes hace una promesa que la versión con variables nunca llega a formular: cada paso recibe un valor y devuelve un valor, aquí dentro no pasa nada más. Los efectos secundarios no encajan en la forma, y en la revisión de código ese desajuste se ve desde la otra punta de la sala. Cuando un colega intenta colar una llamada al logger en mitad de una transformación, la propia cadena protesta. Con cuatro variables temporales, esa misma llamada al logger se cuela entre asignaciones y nadie pestañea. El operador no te deja hacer nada nuevo. Cambia qué es lo que parece mal, y tras suficientes años manteniendo código de sanitización ajeno, he llegado a valorar eso más que la comodidad del depurador.

Así que mi regla de trabajo desde que aterrizó 8.5: pipes para cualquier cosa que sea honestamente una transformación, constructores de slugs, sanitizadores de entrada, el baile de filter, map y unique sobre arrays. Variables de toda la vida para cualquier cosa con ramificaciones, returns tempranos o uso intensivo de estado de objeto, donde una cadena sería solo un disfraz. Las herramientas no te van a poner pegas, tanto PhpStorm como Intelephense en VS Code entienden la sintaxis a principios de 2026. Y si una cadena acumula más de dos closures inline, me paro y extraigo funciones con nombre antes de mergear, porque esa cadena me está diciendo que el dominio tiene vocabulario que todavía no he puesto por escrito.

La gente de Elixir tiene |> desde antes de que parte de nuestro código legacy fuera legacy, y F# desde antes aún, así que PHP llega tarde a esta fiesta en concreto. Vale. Llegamos con optimización a nivel de opcache y callables de primera clase ya en la caja de herramientas, que es una forma decente de llegar tarde. Mi pregunta abierta va sobre 8.6: cuando aterrice la aplicación parcial y los envoltorios (fn($s) => ...) se evaporen, ¿seguirás extrayendo pasos con nombre, o el impuesto de los paréntesis era lo único que mantenía honestos tus pipelines? Dicho de otro modo, ¿cuántas closures inline permites en una cadena antes de bloquear el merge request? Yo digo dos. Dime por qué me equivoco.