PHP lleva años añadiendo operadores que hacen en un símbolo lo que
antes eran cinco líneas de if. Y sin embargo sigues viendo
isset() anidados y usort con tres if dentro. Estos
cuatro —??, ?->, ... y <=>—
están en el lenguaje desde hace tiempo, no hace falta instalar nada, y cada uno borra un
tipo de ruido distinto.
?? — el que asume que puede no haber nada
El null coalescing (PHP 7.0) devuelve lo de la izquierda si existe y no es
null; si no, lo de la derecha. La gracia está en que no avisa cuando
la clave o la variable no existe: lleva un isset() dentro.
// antes
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
// ahora
$page = (int) ($_GET['page'] ?? 1);
Se encadena, y evalúa de izquierda a derecha hasta encontrar el primero que no sea nulo:
$locale = $request->query->get('lang')
?? $user?->getLocale()
?? $_ENV['DEFAULT_LOCALE']
?? 'es';
Y tiene una versión de asignación, ??= (PHP 7.4), que solo asigna si el destino
está vacío. Es la forma corta del caché perezoso de toda la vida:
// antes
if (!isset($this->config['timeout'])) {
$this->config['timeout'] = 30;
}
// ahora
$this->config['timeout'] ??= 30;
Lo que confunde a todo el mundo es la diferencia con ?: (el ternario corto).
No son intercambiables:
$a = ['nombre' => '0'];
$a['nombre'] ?: 'anónimo'; // 'anónimo' ← '0' es falsy
$a['nombre'] ?? 'anónimo'; // '0' ← existe y no es null
$a['apodo'] ?: 'anónimo'; // 'anónimo' + Warning: undefined array key
$a['apodo'] ?? 'anónimo'; // 'anónimo' en silencio
Regla práctica: ?? pregunta «¿existe?», ?: pregunta «¿tiene
valor?». Para un precio, un contador o un '0' que viene de un formulario, esa
diferencia es un bug esperando a pasar.
?-> — el que no revienta si es null
El nullsafe (PHP 8.0) llama al método o lee la propiedad solo si el objeto
existe. Si es null, devuelve null y sigue adelante en vez de
lanzar el clásico Call to a member function on null.
// antes
$ciudad = null;
if (null !== $pedido) {
$cliente = $pedido->getCliente();
if (null !== $cliente) {
$direccion = $cliente->getDireccion();
if (null !== $direccion) {
$ciudad = $direccion->getCiudad();
}
}
}
// ahora
$ciudad = $pedido?->getCliente()?->getDireccion()?->getCiudad();
Y el detalle que mucha gente no sabe: cortocircuita la cadena entera. En
cuanto uno da null, lo que queda a la derecha ni se ejecuta. No hace falta
poner ?-> en todos los eslabones para estar a salvo, aunque ponerlo solo
donde de verdad puede haber null documenta mejor el código.
Ojo con tres cosas:
- No funciona para escribir.
$a?->b = 1;es error de sintaxis. Solo lee. - No existe para arrays. Para
$datos['user']['name']lo que tienes es??, no?->. - Puede tapar un problema de verdad. Si ese objeto nunca
debería ser
null, el?->convierte una excepción clara en un dato vacío que aparecerá tres pantallas más allá. Úsalo cuando elnullsea un caso legítimo, no para silenciar.
Y un aviso si trabajas con Doctrine: encadenar ?-> sobre relaciones es muy
cómodo, pero cada eslabón puede disparar su propia consulta. Es
una de las formas de acabar con un
N+1 sin haber escrito un solo bucle.
... — el que reparte y el que recoge
El spread hace dos cosas según dónde lo pongas. En la firma de una función recoge argumentos sueltos en un array:
function sumar(int ...$numeros): int
{
return array_sum($numeros);
}
sumar(1, 2, 3); // 6
sumar(...[1, 2, 3]); // 6
En una llamada hace lo contrario: desmonta el array en argumentos. Eso mata al
call_user_func_array:
// antes
$respuesta = call_user_func_array([$servicio, 'crear'], $parametros);
// ahora
$respuesta = $servicio->crear(...$parametros);
También fusiona arrays (PHP 7.4), y para listas es bastante más legible que
array_merge:
$base = ['php', 'symfony'];
$adicionales = ['doctrine', 'twig'];
$todas = [...$base, ...$adicionales, 'redis'];
// ['php', 'symfony', 'doctrine', 'twig', 'redis']
Desde PHP 8.1 admite claves de texto, y ahí gana de verdad, porque combina con la sintaxis de array y las claves repetidas ganan por la derecha:
$porDefecto = ['timeout' => 30, 'reintentos' => 3];
$delCliente = ['timeout' => 5];
$opciones = [...$porDefecto, ...$delCliente];
// ['timeout' => 5, 'reintentos' => 3]
Y desde PHP 8.1 también se puede desplegar un array asociativo como argumentos con nombre, que es una forma muy limpia de construir un objeto desde una configuración:
$datos = ['nombre' => 'Ada', 'email' => 'ada@example.com'];
$usuario = new Usuario(...$datos); // equivale a new Usuario(nombre: 'Ada', email: '…')
La trampa: con claves numéricas, [...$a, ...$b] reindexa,
mientras que array_merge hace lo mismo pero + no. Si dependes de
conservar los índices, ninguno de los dos es tu operador.
<=> — la nave espacial que ordena
El operador spaceship (PHP 7.0) compara dos valores y devuelve
-1, 0 o 1. Justo lo que pide
usort, que es donde vive:
// antes
usort($productos, function (Producto $a, Producto $b) {
if ($a->getPrecio() === $b->getPrecio()) {
return 0;
}
return $a->getPrecio() < $b->getPrecio() ? -1 : 1;
});
// ahora
usort($productos, fn (Producto $a, Producto $b) => $a->getPrecio() <=> $b->getPrecio());
Para invertir el orden, das la vuelta a los operandos: $b <=> $a. Y para
ordenar por varios criterios, el truco bueno: compara arrays. PHP los
recorre elemento a elemento y se queda en el primero que difiere.
// por prioridad (desc), y a igualdad, por fecha (asc) y nombre
usort($tareas, fn (Tarea $a, Tarea $b) =>
[$b->getPrioridad(), $a->getFecha(), $a->getNombre()]
<=>
[$a->getPrioridad(), $b->getFecha(), $b->getNombre()]
);
Eso son tres criterios en una expresión, sin un solo if. Dos detalles: los
arrays deben tener el mismo número de elementos (si no, gana el más largo sin mirar el
contenido), y los objetos DateTimeInterface se comparan bien entre ellos, así
que no hace falta pasarlos a timestamp.
Una cosa más: desde PHP 8 las comparaciones entre cadena y número son sensatas
(0 == 'foo' ya es false), pero <=> sigue
aplicando las reglas de coerción del lenguaje. Comparando tipos distintos, los resultados
pueden sorprender; compara manzanas con manzanas.
El resumen, por si vienes con prisa
| Operador | Desde | Para qué |
|---|---|---|
?? |
7.0 | Valor por defecto cuando la clave puede no existir. |
??= |
7.4 | Asignar solo si aún no hay valor. |
?-> |
8.0 | Cadenas de llamadas donde algún eslabón puede ser null. |
... |
5.6 / 7.4 / 8.1 | Variádicas, desplegar argumentos y fusionar arrays. |
<=> |
7.0 | Comparadores de usort, simples o por varios criterios. |
Lo que se rompe si no lo sabes
??no es?:. Con'0',0,''o un array vacío se comportan distinto, y la mitad de los valores por defecto mal puestos vienen de ahí.?->no es untry/catch. Si el método existe pero lanza una excepción, la excepción sale igual. Solo cubre elnulldel objeto.?->oculta la causa. Unnullque viaja por media aplicación es más difícil de depurar que un error en la línea exacta.- El spread con claves numéricas reindexa. Si el array era un mapa de ids, después ya no lo es.
- Desplegar con nombres falla si sobra una clave.
new Usuario(...$datos)lanzaErrorsi$datostrae una clave que el constructor no acepta. Filtra antes. <=>con arrays de distinta longitud no compara: decide por el tamaño. Construye siempre listas del mismo número de criterios.
Y ya está
Ninguno de los cuatro es una técnica avanzada: son símbolos que llevan años en el lenguaje
y que están ahí precisamente para quitar el andamiaje. La próxima vez que escribas un
isset() ternario, tres if por un null o un
comparador de seis líneas, prueba si el operador lo dice más corto. Casi siempre sí.