🐘 PHP 8.5: Pequeños cambios, grandes mejoras
Bueno porfin he acabado mi recopilación de algunos patrones de diseño. Acabé un poco cansado, por no decir hasta las ****** y ahora vamos a hablar de otras cositas como que… ¡Ya salió la versión 8.5 de PHP!. A diferencia de otras versiones que rompían todo, esta actualización se centra en hacernos la vida más bonita a los desarrolladores. Viene cargada de “Quality of Life improvements”, es decir, mejoras que hacen que escribir código sea menos tedioso y más limpio.
Aquí te resumo lo más interesante de forma muy simple.

Resumen rápido de novedades
Antes de entrar al lío, aquí tienes un resumen rapidito de lo que trae PHP 8.5:
Encadena funciones de forma legible, de izquierda a derecha, como en Linux o lenguajes funcionales.
Funciones nativas para obtener el primer y último elemento de un array. Adiós a los hacks con reset().
Clona objetos modificando propiedades en una sola línea. Perfecto para inmutabilidad.
Manejo nativo de URLs cumpliendo RFC 3986 y WHATWG. Adiós parse_url() chapucero.
Techo duro de memoria que ni ini_set() puede saltarse. Ideal para hosting y estabilidad.
Marca funciones cuyo valor de retorno no debe ignorarse. PHP te avisa si lo haces.
Ahora puedes marcar constantes de clase y traits como obsoletos directamente con atributos.
Se deprecan casteos antiguos como (integer), el operador backtick y null como índice de array.
🚀 El operador pipe |>
Si acostumbras a usar la terminal de Linux, esto te va a encantar. El operador Pipe (|>) nos permite encadenar funciones de una manera mucho más legible.
El Problema de antes
Hasta ahora si querías aplicar varias funciones a un valor, acababas con un popurrí de paréntesis difícil de leer:
$resultado = strtolower(trim(htmlentities($input)));
Se puede apreciar un lindo follón bastante complicado de leer, y es un ejemplo bastante usual. Además, se lee al revés: la primera función que se ejecuta (htmlentities) es la que está más a la derecha. Un lío.
La solución (ahora)
Con el operador Pipe, pasamos el valor de una función a la siguiente, de izquierda a derecha (o de arriba a abajo), como si fuera una tubería:
$resultado = $input
|> htmlentities(...)
|> trim(...)
|> strtolower(...);
Mucho más limpio, ¿verdad? El valor de la izquierda se pasa automáticamente como primer argumento a la función de la derecha.
Cómo funciona por debajo
El operador Pipe utiliza la sintaxis First-Class Callable (...) que ya conocemos de PHP 8.1. Cada función se convierte en un Closure que recibe un solo argumento:
// El pipe pasa el resultado como primer argumento
$resultado = " Hola Mundo "
|> trim(...) // trim(" Hola Mundo ") → "Hola Mundo"
|> strtoupper(...) // strtoupper("Hola Mundo") → "HOLA MUNDO"
|> str_split(...); // str_split("HOLA MUNDO") → ['H','O','L','A',' ','M','U','N','D','O']
Si necesitas pasar más argumentos, puedes usar una arrow function:
$resultado = $input
|> trim(...)
|> fn($x) => str_replace(' ', '-', $x)
|> strtolower(...);
Y sí, también funciona con tus propias funciones y métodos estáticos:
function limpiar(string $texto): string {
return preg_replace('/[^a-zA-Z0-9\s]/', '', $texto);
}
$slug = $titulo
|> limpiar(...)
|> trim(...)
|> fn($x) => str_replace(' ', '-', $x)
|> strtolower(...);
El ícono sale representado como
|>pero realmente es |>, que se convierte automáticamente y no deja ver los cáracteres reales.
¿Qué hace el operador Pipe (|>) en PHP 8.5?
🔍 array_first() y array_last()
Después de mucho tiempo pidiéndolo, PHP incluye funciones nativas para obtener el primer y el último elemento de un array sin tener que hacer malabares con reset(), end() o array_key_first().
Antes
$frutas = ['manzana', 'pera', 'plátano'];
// Para sacar el primero...
$primera = reset($frutas); // Pero cuidado, esto mueve el puntero interno...
// O la forma segura pero fea:
$primera = $frutas[array_key_first($frutas)];
Ahora
$frutas = ['manzana', 'pera', 'plátano'];
$primera = array_first($frutas); // 'manzana'
$ultima = array_last($frutas); // 'plátano'
Simple y directo, como debe ser.
Con callback para filtrar
Lo que mucha gente no sabe es que estas funciones aceptan un callback opcional para buscar el primer o último elemento que cumpla una condición:
$numeros = [1, 2, 3, 4, 5, 6, 7, 8];
// Primer número par
$primerPar = array_first($numeros, fn($valor) => $valor % 2 === 0);
// $primerPar = 2
// Último número impar
$ultimoImpar = array_last($numeros, fn($valor) => $valor % 2 !== 0);
// $ultimoImpar = 7
El callback recibe el valor y opcionalmente la clave como segundo argumento. Además, puedes pasar un valor por defecto como tercer argumento si no se encuentra ninguna coincidencia:
$usuarios = ['Ana' => 28, 'Luis' => 17, 'Marta' => 32];
// Primer usuario mayor de 50 años, con valor por defecto
$resultado = array_first($usuarios, fn($edad) => $edad > 50, 'No encontrado');
// $resultado = 'No encontrado'
Esto nos ahorra bucles foreach con break que hacíamos hasta ahora para lo mismo.
¿Qué devuelve array_first() si le pasas un callback que no encuentra coincidencias y no defines un valor por defecto?
👯 Clonar con modificaciones (clone with)
Si trabajas con objetos inmutables, sabrás que clonarlos para cambiar solo una propiedad era un poco rollo.
Antes
Tenías que clonar el objeto y luego, de alguna manera mágica (o con métodos with...), cambiar la propiedad.
$coche = new Coche('Rojo', 'V8');
$cocheAzul = clone $coche;
$cocheAzul->color = 'Azul'; // Esto fallaba si la propiedad era readonly
Ahora
PHP 8.5 introduce una sintaxis preciosa para esto:
$coche = new Coche('Rojo', 'V8');
// Clona el coche y cambia SOLO el color en el nuevo objeto
$cocheAzul = clone $coche with {
color: 'Azul'
};
Esto es genial para mantener nuestros objetos seguros e inmutables pero fáciles de trabajar.
Ejemplo práctico con DTOs
Donde más vas a usar esto es con DTOs (Data Transfer Objects) y objetos de configuración. Imagina un caso típico con readonly:
readonly class PedidoDTO {
public function __construct(
public string $producto,
public int $cantidad,
public string $estado,
public ?string $trackingId = null,
) {}
}
$pedido = new PedidoDTO('Teclado mecánico', 2, 'pendiente');
// Actualizar estado sin tocar el resto
$pedidoEnviado = clone $pedido with {
estado: 'enviado',
trackingId: 'ES123456789',
};
Antes de esto, para hacer lo mismo con una clase readonly, tenías que crear un método with() manual por cada propiedad o usar reflection. Un rollo tremebundo.
Con el método __clone()
Si tu clase tiene el método mágico __clone(), este se ejecuta antes de aplicar las modificaciones del with. Es decir, primero se clona el objeto (ejecutando __clone()) y luego se aplican los cambios. Tenlo en cuenta si tienes lógica en ese método.
¿Qué ventaja tiene 'clone with' sobre clonar y modificar manualmente?
¿Cuándo se ejecuta __clone() al usar 'clone ... with'?
🌐 Nueva Extensión URI
PHP 8.5 introduce una extensión nativa para manejar URLs de forma robusta. Olvídate de los problemas con parse_url(). Esta nueva extensión cumple con los estándares RFC 3986 y WHATWG.
$uri = new Uri\Rfc3986\Uri('https://usuario:pass@ejemplo.com:8080/ruta?query=1#fragmento');
echo $uri->getHost(); // ejemplo.com
echo $uri->getPort(); // 8080
echo $uri->getScheme(); // https
echo $uri->getPath(); // /ruta
echo $uri->getQuery(); // query=1
// Modificar la URL es súper sencillo e inmutable
$nuevaUri = $uri->withPath('/nueva-ruta');
¿Por qué no vale parse_url()?
parse_url() tiene problemas conocidos: no valida las URLs, no cumple ningún estándar RFC, y acepta entradas malformadas sin quejarse. Con la nueva extensión URI:
- Cumple RFC 3986 y WHATWG
- Valida la URL al parsearla
- API inmutable y orientada a objetos
- Maneja encoding automáticamente
- No cumple ningún estándar RFC
- No valida la URL
- Devuelve un array (nada de OOP)
- Acepta URLs malformadas sin error
Dos modos: RFC 3986 y WHATWG
La extensión soporta dos estándares distintos según tus necesidades:
// RFC 3986: para APIs, backends, especificaciones estrictas
$uri = new Uri\Rfc3986\Uri('https://api.ejemplo.com/v2/users');
// WHATWG: para URLs de navegador, compatibilidad web
$url = new Uri\WhatWg\Url('https://ejemplo.com/página con espacios');
La diferencia principal es que WHATWG es más permisivo mientras que RFC 3986 es estricto y rechaza URLs que no cumplan el estándar al pie de la letra. Para APIs y backends usa RFC 3986. Para parsear URLs que vengan de usuarios o navegadores, WHATWG.
¿Cuál es la diferencia principal entre Uri\\Rfc3986 y Uri\\WhatWg?
🧠 Límite de Memoria Estricto (max_memory_limit)
¿Alguna vez has tenido un script que se come memoria y se salta los límites? Seguro que si, yo un montón. Ahora tenemos una directiva INI llamada max_memory_limit.
Esta directiva actúa como un techo duro. Incluso si un script intenta subir su propio límite con ini_set('memory_limit', '-1'), no podrá superar lo que diga max_memory_limit. Es ideal para proveedores de hosting o para asegurar la estabilidad de tu servidor.
O para ti, chapucillas, que siempre la acabas liando.
Cómo configurarlo
; php.ini
memory_limit = 128M ; El límite "normal" por script
max_memory_limit = 512M ; El techo que NADIE puede superar
Con esto, un script puede subir su memory_limit hasta 512M con ini_set(), pero nunca por encima. Si intenta poner -1 (ilimitado), PHP lo limita silenciosamente a 512M.
Caso de uso típico: hosting compartido
En un servidor con varios clientes, cada uno puede tener su memory_limit configurado en su .htaccess o por pool de PHP-FPM. Pero con max_memory_limit en el php.ini global, te aseguras de que ningún cliente pueda reventar el servidor entero poniendo -1. Si eres el admin del servidor, esto te va a salvar la vida.
¿Qué ocurre si un script intenta poner memory_limit a -1 teniendo max_memory_limit configurado?
✨ Mejoras en Atributos
Los atributos siguen ganando poder. Aquí las novedades:
#[Override] en propiedades
Ya conocíamos #[Override] para métodos (desde PHP 8.3), y ahora se extiende a propiedades. Si marcas una propiedad con #[Override], PHP se asegura de que estás sobrescribiendo una propiedad de la clase padre. Si la propiedad padre desaparece en una actualización, te salta un error:
class Animal {
public string $nombre = 'Sin nombre';
}
class Perro extends Animal {
#[Override]
public string $nombre = 'Rex'; // OK: sobrescribe Animal::$nombre
}
class Gato extends Animal {
#[Override]
public string $raza = 'Siamés'; // ERROR: Animal no tiene $raza
}
Esto va a ir muy bien para mantener jerarquías de clases consistentes.
#[Deprecated] en constantes y traits
Marca constantes o traits enteros como obsoletos para avisar a otros desarrolladores:
class MiClase {
#[Deprecated("Usa NUEVA_CONSTANTE en su lugar")]
public const VIEJA_CONSTANTE = 1;
public const NUEVA_CONSTANTE = 2;
}
// Al usar VIEJA_CONSTANTE, PHP emite un E_USER_DEPRECATED
echo MiClase::VIEJA_CONSTANTE; // ⚠️ Deprecated: Usa NUEVA_CONSTANTE en su lugar
También funciona con traits:
#[Deprecated("Usa LoggerInterface en su lugar")]
trait LogTrait {
public function log(string $msg): void {
echo $msg;
}
}
class MiServicio {
use LogTrait; // ⚠️ Deprecated: Usa LoggerInterface en su lugar
}
⚠️ Atributo #[NoDiscard]
A veces creamos funciones que devuelven algo importante, y si el programador ignora ese resultado, es probable que esté cometiendo un error. Imagina una función que te devuelve un error si algo falla, pero no lanza una excepción. Si ignoras el resultado, no te vas a enterar del fallo.
Ahora puedes marcar tus funciones con #[NoDiscard]:
#[NoDiscard]
function conectarBaseDatos(): bool {
// ... lógica ...
return false; // Falló
}
// Si haces esto, PHP te avisará:
conectarBaseDatos(); // ⚠️ Warning: Unused return value
Es una forma de decirle al desarrollador: ¡Eh Pavo! Lo que te devuelvo es importante, no lo ignores, que te reviento.
Ejemplo con mensaje personalizado
Puedes pasar un mensaje explicando por qué es importante no ignorar el resultado:
#[NoDiscard("El resultado contiene errores de validación que debes comprobar")]
function validarFormulario(array $datos): array {
$errores = [];
if (empty($datos['email'])) {
$errores[] = 'El email es obligatorio';
}
return $errores;
}
// PHP avisa con tu mensaje personalizado:
validarFormulario($datos); // ⚠️ Warning: El resultado contiene errores de validación...
¿Dónde usarlo?
Es especialmente útil en funciones que:
- Devuelven errores sin lanzar excepciones
- Devuelven recursos que deben cerrarse después (file handles, conexiones…)
- Devuelven resultados de operaciones que determinan el flujo del programa
PHP ya ha marcado varias funciones internas con #[NoDiscard], así que es posible que veas nuevos warnings en tu código existente al actualizar.
¿Qué ocurre si marcas una función con #[NoDiscard] y alguien ignora su valor de retorno?
🗑️ Deprecateds y Eliminaciones
Como en toda limpieza, hay cosas que se van. Aquí lo más importante que debes revisar:
| Qué se deprecia | Alternativa | Impacto |
|---|---|---|
(boolean), (integer), (double), (binary) | (bool), (int), (float), (string) | Alto si usas los casteos largos |
Operador backtick `comando` | shell_exec('comando') | Medio |
null como índice de array | Usa "" (string vacío) o comprueba antes | Alto (muy común sin darte cuenta) |
SplFixedArray::__wakeup() | __unserialize() | Bajo |
E_STRICT | Ya no se emite, usa E_ALL | Bajo |
Cuidado con null como índice
Este es el que más gente va a pillar desprevenida. Cosas como estas ahora generan un E_DEPRECATED:
$array = [];
$key = null;
$array[$key] = 'valor'; // ⚠️ Deprecated
isset($array[$key]); // ⚠️ Deprecated
array_key_exists($key, $array); // ⚠️ Deprecated
PHP convertía null a "" (string vacío) silenciosamente. Ahora te avisa. Revisa tu código porque es más común de lo que crees, especialmente cuando recibes datos de formularios o APIs donde un campo puede venir como null.
¿Por qué la depreciación de null como índice de array puede ser problemática?
Tabla resumen: evolución de PHP 8.x
Para que veas de un vistazo cómo ha ido evolucionando PHP en las últimas versiones:
| Versión | Feature estrella | Año |
|---|---|---|
| PHP 8.0 | Named arguments, Union types, Match, JIT | 2020 |
| PHP 8.1 | Enums, Fibers, Readonly properties, Intersect types | 2021 |
| PHP 8.2 | Readonly classes, DNF types, true/false/null types | 2022 |
| PHP 8.3 | Typed constants, #[Override], json_validate() | 2023 |
| PHP 8.4 | Property hooks, Asymmetric visibility, new without parentheses | 2024 |
| PHP 8.5 | Pipe operator, clone with, Extensión URI, #[NoDiscard] | 2025 |
Tips para actualizar a PHP 8.5
Antes de darle al botón de actualizar como un loco, tenlo en cuenta:
Busca (integer), (boolean), (double) y (binary) en tu código y reemplázalos por (int), (bool), (float) y (string).
Si usas `comando` en algún sitio, cámbialo a shell_exec('comando') o mejor aún, a Process de Symfony.
Usa herramientas de análisis estático como PHPStan o Psalm en nivel alto para detectar usos de null como clave de array.
Ejecuta composer outdated y revisa que tus paquetes soporten PHP 8.5. Especialmente ORMs, frameworks y SDKs de terceros.
✅ Conclusiones
PHP 8.5 no reinventa la rueda, pero la hace girar mucho más suave. El operador Pipe y las funciones de array son cosas que usaremos todos los días. clone with es un regalo para los que trabajamos con objetos inmutables y DTOs. Y la extensión URI por fin estandariza algo que parse_url() hacía a medias desde hace 20 años.
Lo mejor de esta versión es que puedes adoptar las novedades poco a poco. No hay breaking changes gordos, solo depreciaciones que puedes ir arreglando con calma.
Pos EA nos vemos en los bares chavales! 🍻