La semana pasada una lectora me mandó un diff con una línea marcada en rojo: `$tag = sprintf('[%s] %s', currentRequestId(), ?);`. Su pregunta: «¿Esto es lo mismo que la arrow function a la que sustituye?». No lo es, y esa sola línea resume casi todo lo que necesitas saber sobre la aplicación parcial de funciones en PHP 8.6. La funcionalidad me gusta mucho y la quiero en nuestros proyectos desde el primer día. Pero también creo que la mayoría la vamos a leer mal durante los primeros seis meses, porque parece azúcar sintáctico de `fn` y en realidad se comporta como un constructor que saca una foto fija.
Un repaso rápido por si te saltaste los hilos del RFC. PHP 8.6 está en beta y su lanzamiento está previsto para alrededor del 19 de noviembre de 2026. PFA se apoya en la sintaxis de callables de primera clase `trim(...)` que llegó en 8.1. Llamas a una función o a un método de forma normal, pero pones un `?` en cada hueco que quieras rellenar más tarde, y obtienes un callable. Un `?` representa exactamente un argumento. Un `...` al final significa todos los argumentos restantes, y tiene que ir el último, después de los huecos posicionales y de los nombrados. Si escribes `str_replace(' ', '-', ?)` tienes un generador de slugs sin closure, sin un nombre de parámetro inventado y sin repetir `$value` dos veces. Funciona también con métodos estáticos y de instancia, y se lleva bien con los argumentos nombrados, así que `Book::create(category: Category::THRILLER, title: ?)` se lee casi como configuración.
Esta es la mitad que le faltaba al operador pipe. Desde que llegó `|>`, todos los pipelines que he escrito tenían un eslabón feo: la arrow function entre paréntesis que necesitas en cuanto un paso recibe más de un argumento. Cinco etapas limpias y, en medio, un `(fn ($v) => str_replace(' ', '-', $v))` metido ahí como una brida de plástico. Con PFA ese paso se convierte en `str_replace(' ', '-', ?)` y el `?` indica exactamente por dónde entra el valor. A la gente de Go le encanta decirnos que los bucles explícitos son mejores que encadenar cosas con ingenio, y algo de razón tienen. Pero si un lenguaje tiene pipe, el pipe necesita esto, y 8.6 lo trae.
Volvamos al círculo rojo. Una parcial evalúa todos los argumentos que no son marcadores en el momento en que la creas. El valor queda capturado y se reutiliza en cada llamada. Así que el `currentRequestId()` de la línea de mi lectora se ejecutó exactamente una vez, cuando el worker arrancó y construyó su formateador. La arrow function a la que sustituía llamaba a esa función en cada línea de log. En una petición clásica de FPM nadie se entera, porque la parcial y la petición mueren juntas. En un worker de larga duración con Swoole, RoadRunner o una cola, cada línea de log de los siguientes cuarenta mil trabajos lleva el ID del primero. Quien esté de guardia lo descubre a las 2 de la madrugada, buscando con grep una petición que parece haber atendido todo el tráfico de la noche.
La objeción más fuerte merece tomarse en serio. Podrías decir que esto demuestra que PFA es una forma de pegarse un tiro en el pie, que `?` es un símbolo más en un lenguaje que ya va sobrado de ellos, y que una closure explícita es más honesta porque te muestra exactamente qué se ejecuta y cuándo. Estoy de acuerdo a medias. Una closure deja claro el momento de ejecución y una parcial lo esconde. Pero la captura anticipada es la semántica correcta, y muchas veces es justo lo que quieres: construir lo caro una vez y llamarlo muchas. La versión con closure que vuelve a ejecutar `getPrefix()` en cada llamada tiene sus propios bugs, solo que tienen otra cara. Lo que saco de la objeción es que necesitamos un hábito de revisión. No hace falta prohibir la sintaxis.
La segunda trampa es más silenciosa. Una parcial es una closure completamente nueva. No se limita a apuntar a la función original como hace `strlen(...)`. Los atributos personalizados de la original no se trasladan. Las excepciones son `#[\NoDiscard]` y `#[\SensitiveParameter]`, y este último solo sobrevive en los parámetros que dejaste abiertos. Si tu framework usa reflexión sobre los callables para encontrar metadatos de rutas, reglas de validación o marcas de event listeners, una parcial se le colará sin hacer ruido. Si tu equipo configura el contenedor a base de atributos, haz un grep antes de dejar que nadie registre parciales.
Así que esta es la norma que voy a proponer en mi equipo. Un `?` o un `...` final está bien en cualquier sitio donde los argumentos fijos sean literales, constantes, casos de enum o valores que ya están en variables locales. En cuanto un argumento fijo sea una llamada a función, una llamada a método o cualquier cosa que lea el reloj, la petición o el contenedor, quien escribe el código o bien lo sube a una variable con nombre en la línea anterior, para que la captura se vea, o bien escribe una closure a propósito. Las parciales nunca van a ningún sitio donde Reflection vaya a leer atributos. Nos cuesta una línea de más de vez en cuando y, a cambio, nadie tiene que adivinar cuándo se evalúa un argumento.
Aun así, no tengo claro que la norma sea la correcta. Puede que sea demasiado prudente para los pipelines, donde casi todos los argumentos fijos son literales de todas formas. Y puede que se quede corta para los workers de larga duración. Así que cuéntamelo en los comentarios: cuando 8.6 salga en noviembre, ¿tu equipo va a permitir llamadas a funciones como argumentos fijos en una parcial, o va a exigir que los valores capturados se suban antes a una variable?




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.