PHP 8.0: Resumen completo de las novedades (1/4)

hace 4 años por David Grudl  

Ha salido la versión 8.0 de PHP. Está tan repleta de novedades como ninguna versión anterior. Su presentación ha requerido cuatro artículos separados. En este primero, veremos qué novedades aporta en cuanto al lenguaje.

Antes de sumergirnos en PHP, debe saber que la versión actual de Nette está totalmente preparada para la versión 8. Es más, como regalo, incluso se ha lanzado Nette 2.4, que es totalmente compatible con ella, por lo que desde el punto de vista del framework, nada le impide empezar a usar la nueva versión.

Argumentos nombrados

Y empezamos directamente con una bomba, que se puede calificar audazmente como un game changer. Ahora se pueden pasar argumentos a funciones y métodos no solo posicionalmente, sino también por sus nombres. Lo cual es absolutamente genial en el caso de que un método tenga realmente muchos parámetros:

class Response implements IResponse
{
	public function setCookie(
		string $name,
		string $value,
		string|DateInterface|null $time,
		string $path = null,
		string $domain = null,
		bool $secure = null,
		bool $httpOnly = null,
		string $sameSite = null
	) {
		...
	}
}

Pasamos los dos primeros argumentos posicionalmente, los siguientes por nombre: (los nombrados siempre deben seguir a los posicionales)

$response->setCookie('lang', $lang, sameSite: 'None');

// en lugar del demencial
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Antes de la llegada de esta característica, estaba previsto crear una nueva API en Nette para enviar cookies, ya que el número de parámetros de setCookie() realmente había crecido y la notación posicional era confusa. Ahora ya no es necesario, porque los argumentos nombrados son en este caso la API más práctica. El IDE los sugerirá y tienen control de tipos.

También son geniales para aclarar parámetros lógicos, donde su uso no es necesario, pero true o false por sí solos no dicen mucho:

// antes
$db = $container->getService(Database::class, true);

// ahora
$db = $container->getService(Database::class, need: true);

Los nombres de los parámetros ahora forman parte de la API pública. No es posible cambiarlos arbitrariamente como hasta ahora. Por esta razón, Nette también está pasando por una auditoría para verificar si todos los parámetros tienen una nomenclatura adecuada.

Los argumentos nombrados también se pueden usar en combinación con variadics:

function variadics($a, ...$args) {
	dump($args);
}

variadics(a: 1, b: 2, c: 3);
// en $args estará ['b' => 2, 'c' => 3]

Ahora, el array $args puede contener también claves no numéricas, lo cual es un cierto BC break. Lo mismo se aplica al comportamiento de la función call_user_func_array($func, $args), donde ahora las claves en el array $args juegan un papel significativo. Por el contrario, las funciones de la familia func_*() están aisladas de los argumentos nombrados.

Con los argumentos nombrados también está estrechamente relacionado el hecho de que el operador splat ... ahora puede desempaquetar también arrays asociativos:

variadics(...['b' => 2, 'c' => 3]);

Sorprendentemente, esto aún no funciona dentro de arrays:

$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys

La combinación de argumentos nombrados y variadics da la posibilidad de tener finalmente una sintaxis fija, por ejemplo, para el método del presenter link(), al que ahora podemos pasar argumentos nombrados de la misma manera que los posicionales:

// antes
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // tenía que ser un array

// ahora
$presenter->link('Product:detail', $id, page: 1);

La sintaxis para argumentos con nombre es mucho más sexy que escribir matrices, así que “Latte la adoptó inmediatamente:https://blog.nette.org/…ot-for-least#…”, donde puede utilizarse, por ejemplo, en las etiquetas {include} y {link}:

{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}

Volveremos a los parámetros nombrados en la tercera parte en relación con los atributos.

La expresión puede lanzar una excepción

Lanzar una excepción es ahora una expresión. Puede, por ejemplo, envolverla en paréntesis y añadirla a una condición if. Hmmm, eso no suena muy práctico. Pero esto ya es más interesante:

// antes
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('value not set');
}
$value = $arr['value'];


// ahora, cuando throw es una expresión
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Dado que las funciones de flecha hasta ahora solo pueden contener una expresión, gracias a esta característica pueden lanzar excepciones:

// only single expression
$fn = fn() => throw new \Exception('oops');

Expresiones Match

La construcción switch-case tiene dos grandes defectos:

  • utiliza comparación no estricta == en lugar de ===
  • debe tener cuidado de no olvidar accidentalmente un break

Por lo tanto, PHP presenta una alternativa en forma de la nueva construcción match, que utiliza comparación estricta y, por el contrario, no utiliza break.

Ejemplo de código switch:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('not found');
        break;
    case 500:
        $message = $this->formatMessage('server error');
        break;
    default:
        $message = 'unknown status code';
        break;
}

Y lo mismo (solo con comparación estricta) escrito usando match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('not found'),
    500 => $this->formatMessage('server error'),
    default => 'unknown status code',
};

Observe que match no es una estructura de control como switch, sino una expresión. En el ejemplo, asignamos su valor resultante a una variable. Al mismo tiempo, las “opciones” individuales también son expresiones, por lo que no se pueden escribir múltiples pasos, como en el caso de switch.

Si no hay coincidencia con ninguna de las opciones (y no existe la cláusula default), se lanza una excepción UnhandledMatchError.

Por cierto, en Latte también existen las etiquetas {switch}, {case} y {default}. Su funcionamiento corresponde exactamente al nuevo match. Utilizan comparación estricta, no requieren break y en case es posible indicar múltiples valores separados por comas.

Operador Nullsafe

El encadenamiento opcional (optional chaining) permite escribir una expresión cuya evaluación se detiene si encuentra un null. Y esto gracias al nuevo operador ?->. Reemplaza mucho código que de otro modo verificaría repetidamente si hay null:

$user?->getAddress()?->street

// significa aproximadamente
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

¿Por qué “significa aproximadamente”? Porque en realidad, la expresión se evalúa de manera más ingeniosa y ningún paso se repite. Por ejemplo, $user->getAddress() se llama solo una vez, por lo que no puede surgir el problema causado por el hecho de que el método devuelva algo diferente la primera y la segunda vez.

Esta fresca novedad la trajo Latte hace un año. Ahora llega al propio PHP. Genial.

Promoción de propiedades del constructor

Azúcar sintáctico que ahorra escribir el tipo dos veces y la variable cuatro veces. Lástima que no llegara en la época en que no teníamos IDEs tan inteligentes que hoy lo escriben por nosotros 🙂

class Facade
{
	private Nette\Database\Connection $db;
	private Nette\Mail\Mailer $mailer;

	public function __construct(Nette\Database\Connection $db, Nette\Mail\Mailer $mailer)
	{
		$this->db = $db;
		$this->mailer = $mailer;
	}
}
class Facade
{
	public function __construct(
		private Nette\Database\Connection $db,
		private Nette\Mail\Mailer $mailer,
	) {}
}

Funciona con Nette DI, puede empezar a usarlo de inmediato.

Comportamiento estricto de los operadores aritméticos y bitwise

Lo que una vez catapultó a los lenguajes de scripting dinámicos a la fama, con el tiempo se convirtió en su punto más débil. PHP se deshizo en su día de las “magic quotes”, del registro de variables globales, y ahora el comportamiento laxo está siendo reemplazado por la rigurosidad. La época en la que en PHP podía sumar, multiplicar, etc., casi cualquier tipo de dato, incluso cuando no tenía ningún sentido, ha quedado atrás. A partir de la versión 7.0, PHP es cada vez más estricto, y desde la versión 8.0, intentar usar cualquier operador aritmético/bitwise en arrays, objetos o resources termina en un TypeError. La excepción es la suma de arrays.

// operadores aritméticos y bitwise
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Comparación más sensata de cadenas y números

O dicho de otro modo, make loose operator great again.

Parecería que para el operador laxo == ya no hay lugar, que es solo un error tipográfico al escribir ===, pero este cambio lo devuelve al mapa. Ya que lo tenemos, que se comporte de manera razonable. La consecuencia de la anterior comparación “no razonable” era, por ejemplo, el comportamiento de in_array(), que podía jugarle una mala pasada:

$validValues = ['foo', 'bar', 'baz'];
$value = 0;

dump(in_array($value, $validValues));
// sorprendentemente devolvía true
// desde PHP 8.0 devuelve false

El cambio en el comportamiento de == se refiere a la comparación de números y cadenas “numéricas” y lo muestra la siguiente tabla:

Comparación Antes PHP 8.0
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42 " true true
42 == "42foo" true false
42 == "abc42" false false
"42" == " 42" true true
"42" == "42 " false true

Sorprendentemente, se trata de un BC break en la base misma del lenguaje, que fue aprobado sin ninguna oposición. Y eso es bueno. Aquí JavaScript podría tener mucha envidia.

Reporte de errores

Muchas funciones internas ahora lanzan TypeError y ValueError en lugar de advertencias, que se podían pasar por alto fácilmente. Se han reclasificado numerosas advertencias del núcleo. El operador shutup @ ahora no silencia los errores fatales. Y PDO en modo predeterminado lanza excepciones.

Estas cosas siempre Nette ha intentado resolverlas de alguna manera. Tracy modificaba el comportamiento del operador shutup, Database cambiaba el comportamiento de PDO, Utils contiene reemplazos de funciones estándar que lanzan excepciones en lugar de advertencias discretas, etc. Es bueno ver que la dirección estricta, que Nette tiene en su ADN, se está convirtiendo en la dirección nativa del lenguaje.

Arrays con índice negativo

$arr[-5] = 'first';
$arr[] = 'second';

¿Cuál será la clave del segundo elemento? Antes era 0, desde PHP 8 es -4.

Coma final

El último lugar donde no podía haber una coma final era la definición de parámetros de función. Eso ya es cosa del pasado:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // coma final
	) {
		....
	}

$object::class

La constante mágica ::class funciona también con objetos $object::class, reemplazando así por completo la función get_class().

catch sin variable

Y finalmente: en la cláusula catch no es necesario indicar la variable para la excepción:

try {
	$container->getService(Database::class);

} catch (MissingServiceException) {  // sin $e
	$logger->log('....');
}

En las próximas entregas nos esperan novedades fundamentales en los tipos de datos, mostraremos qué son los atributos, qué nuevas funciones y clases han aparecido en PHP y presentaremos el Just in Time Compiler.