Durante unos cuatro meses, uno de nuestros paneles de administración mostraba «página 1 de 0» cada vez que una búsqueda no devolvía nada. Nadie abrió un ticket. La línea culpable era max(1, min($lastPage, $requestedPage)), que te devuelve 1 cuando $lastPage vale 0 y sigue tan tranquila. Técnicamente nunca hubo nada roto. Simplemente estaba un poco mal, y mal en silencio, así que nunca nos dimos cuenta. PHP 8.6.0 RC3 se etiquetó el 8 de octubre y la versión estable está prevista para el 19 de noviembre. Después de repasar los cambios, lo tengo claro: lo más valioso de esta versión no es una novedad de sintaxis, sino un carácter. El lenguaje se está volviendo más ruidoso ante las entradas incorrectas, y cada excepción nueva con la que te topes al actualizar es un informe de bug que no has tenido que escribir tú.

Fíjate en la nueva función clamp(). Sería un apaño perfecto en una sola línea para mi panel, pero se niega a seguirte el juego cuando $min es mayor que $max, o cuando cualquiera de los límites es NAN: lanza un ValueError. En mi caso, eso es justo la situación de los resultados vacíos, así que el bug con el que conviví durante meses habría saltado la primera vez que alguien buscara cualquier tontería. El mismo espíritu recorre toda la biblioteca estándar. Muchas funciones nativas que antes se encogían de hombros ante un flag absurdo o un número fuera de rango ahora lanzan un ValueError. preg_grep() devuelve false cuando el motor de expresiones regulares falla durante la ejecución, mientras que antes hacía como si no hubiera pasado nada. base_convert() y sus hermanas emiten un notice cuando no pueden darte un resultado preciso. Y un return dentro de un bloque finally pasa a estar obsoleto, porque siempre se ha tragado en silencio la excepción que iba de salida.

Hasta la sintaxis nueva y reluciente sigue la norma. La aplicación parcial de funciones se aprobó por unanimidad, y un RFC posterior la endureció antes del lanzamiento: cada marcador ? crea un parámetro obligatorio, aunque el parámetro de debajo tenga un valor por defecto. Si escribes round(?, ?), obtienes una closure que exige tanto el número como la precisión, aunque round() se apañe perfectamente sin el segundo. Si quieres conservar los valores por defecto, usa ..., que mantiene opcionales los parámetros opcionales. Esta decisión me encanta. Un signo de interrogación en una llamada debería significar «aquí te voy a dar un valor», y no a veces «o puede que no».

Ahora, el argumento más sólido en contra de todo esto, que me tomo en serio. Desde el punto de vista de tus usuarios, una página que muestra un número un poco raro es mejor que una página que muestra un error. Muchísimo PHP en producción lleva años ajustado, casi siempre sin querer, al viejo comportamiento permisivo. Los equipos con poca cobertura de tests no se enterarán de estas excepciones nuevas en la CI. Se enterarán por los clientes, en pleno despliegue, un viernes. Convertir errores ocultos en fallos visibles está muy bien en teoría. En la práctica, le pasa la factura a quien le toque estar de guardia.

Aun así, me quedo del lado del ruido, y en parte es por lo cuidadoso que ha sido el equipo de core. Los errores de streams ahora pueden lanzarse como StreamException, algo que configuras con la opción error_mode en el contexto del stream, pero por defecto siguen siendo warnings. El file_get_contents() de nadie va a empezar a lanzar excepciones de repente si no lo pide. Las deprecaciones solo emiten E_DEPRECATED, así que tu código sigue funcionando. Y cuando una deprecación parecía dar más trabajo del que valía la pena, la votación dijo que no: list() y el alias de gettext _() se quedan. Eso no es un equipo rompiendo cosas por purismo. Además, la diferencia de coste es real. Una excepción que revienta en staging te cuesta un café y un parche pequeño. Un valor erróneo silencioso te puede costar un año de filas malas que luego tendrás que cuadrar a mano. Todavía quedan RC4 y RC5 en el calendario, así que hay tiempo de apuntar un job de CI a la release candidate y dejar que se queje.

Y aquí viene el giro que me inquieta. La única novedad que falla en silencio es la aplicación parcial de funciones, justo la que tiene a todo el mundo entusiasmado. Los argumentos ligados se evalúan una sola vez, en el momento en que se crea el parcial, y la closure guarda los resultados. Imagina un worker de colas de larga duración que construye un parcial de logging al arrancar y le pasa date('c') como uno de los argumentos. Cada trabajo que procese seis horas después seguirá llevando la marca de tiempo del arranque. No verás ningún error, ni un notice, ni una deprecación. Una arrow function habría llamado a date() en cada invocación. Los desarrolladores de Go pasaron años con una trampa de captura parecida en sus variables de bucle hasta que el lenguaje lo cambió, así que estamos en buena compañía. Pero en una versión que por lo demás tiene tantas ganas de hablar, este es el punto donde nada te dice nada, y yo pondría una regla de revisión o un chequeo de análisis estático para cualquier parcial cuyos argumentos ligados contengan una llamada a función.

Así que te lanzo la pregunta. ¿Dónde habría que trazar la línea? ¿Debería una versión futura, o tus herramientas, avisarte cuando un parcial liga algo que no sea una constante o una variable simple? ¿O es el tipo de ayuda que desactivarías el segundo día? Y si tienes código donde un fallback silencioso es lo que de verdad sostiene el tinglado, donde un ValueError haría más daño que la respuesta incorrecta a la que sustituye, cuéntamelo en los comentarios, porque ese es el argumento más fuerte contra lo que defiendo y me gustaría verlo.