PHP 8.0: Visión completa de las novedades (1/4)

hace 4 años por David Grudl  

La versión 8.0 de PHP está siendo liberada ahora mismo. Está llena de novedades como ninguna otra versión antes. Su introducción merecía cuatro artículos separados. En el primero echaremos un vistazo a lo que trae a nivel de lenguaje.

Antes de adentrarnos en PHP, que conste que la versión actual de Nette está totalmente preparada para la octava versión. Además, como regalo, se ha publicado una Nette 2.4 totalmente compatible, así que desde el punto de vista del framework no hay nada que te impida usarlo.

Argumentos con nombre

Empecemos de inmediato con una bomba, que podría designarse audazmente como un cambio de juego. Los argumentos ahora pueden ser pasados a funciones y métodos no sólo posicionalmente, sino de acuerdo a su nombre. Lo cual es absolutamente genial en caso de que un método tenga realmente demasiados 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
	) {
		...
	}
}

Los dos primeros argumentos se pasan posicionalmente, los otros por su nombre: (el nombre debe ir después de los posicionales)

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

// instead of the horrible
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Antes de la introducción de esta característica, se había planeado crear una nueva API para el envío de cookies en Nette, porque el número de argumentos para setCookie() realmente crecía y la notación posicional era confusa. Eso ya no es necesario, porque los argumentos con nombre son en este caso la API más conveniente. El IDE los indicará y hay seguridad de tipo.

Son ideales incluso para explicar argumentos lógicos, donde su uso no es necesario, pero un simple true o false no es suficiente:

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

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

Los nombres de los argumentos son ahora parte de la API pública. Ya no es posible cambiarlos a voluntad. Debido a esto, incluso Nette está pasando por una auditoría para determinar si todos los argumentos tienen un nombre adecuado.

Los argumentos con nombre también se pueden utilizar en combinación con variadics:

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

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

La matriz $args puede contener ahora incluso claves no numéricas, lo que supone una especie de ruptura de BC. Lo mismo se aplica al comportamiento de call_user_func_array($func, $args), donde las claves de la matriz $args desempeñan ahora un papel mucho más importante. Por el contrario, las funciones de la familia func_*() están protegidas de los argumentos con nombre.

Los argumentos con nombre están estrechamente relacionados con el hecho de que el operador splat ... ahora puede expandir matrices asociativas:

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

Sorprendentemente no funciona dentro de arrays por el momento:

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

La combinación de argumentos con nombre y variadics da la opción de tener finalmente una sintaxis fija, por ejemplo, para el método link(), al que podemos pasar argumentos con nombre así como posicionales:

// before
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // had to be an array

// now
$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 argumentos con nombre en la tercera parte de la serie, en relación con los atributos.

Una expresión puede lanzar una excepción

Lanzar una excepción es ahora una expresión. Puedes envolverla entre paréntesis y añadirla a una condición if. Hmmm, eso no suena muy práctico. Sin embargo, esto es mucho más interesante:

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


// now, when throw is an expression
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Como hasta ahora las funciones de flecha sólo pueden contener una expresión, ahora pueden lanzar excepciones gracias a esta característica:

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

Expresión de coincidencia

La sentencia switch-case tiene dos grandes vicios:

  • utiliza una comparación no estricta == en lugar de ===
  • hay que tener cuidado de no olvidarse por error de break

Debido a esto, PHP viene con una alternativa en forma de una nueva expresión match, que utiliza comparación estricta y, a la inversa, no utiliza break.

switch ejemplo de código:

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 (sólo 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 es posible escribir más pasos, como en el caso de switch.

En caso de no coincidencia (y no hay cláusula por defecto), se lanza la excepción UnhandledMatchError.

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

Operador nulo

El encadenamiento opcional permite escribir una expresión cuya evaluación se detiene si encuentra un valor nulo. Esto es posible gracias al nuevo operador ?->. Sustituye a una gran cantidad de código que, de otro modo, tendría que comprobar repetidamente si es nulo:

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

// approximately translates to
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

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

Esta característica fue traída por Latte hace un año. Ahora el propio PHP la está adoptando. Genial.

Promoción de propiedades del constructor

Azúcar sintáctico que ahorra escribir el tipo dos veces y la variable cuatro. Es una pena que no llegara en una época en la que no tuviéramos IDEs tan listos 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, puedes empezar a usarlo.

Comprobaciones de tipo más estrictas para operadores aritméticos y bitwise

Lo que una vez ayudó a los lenguajes de scripting dinámicos a ascender a la prominencia se ha convertido en su punto más débil. En su día, PHP se deshizo de las “comillas mágicas”, el registro de variables globales y ahora el comportamiento relajado está siendo sustituido por el rigor. La época en la que en PHP se podía añadir, multiplicar, etc. casi cualquier tipo de dato para el que no tuviera sentido, ha quedado atrás. Comenzando con la versión 7.0, PHP se está volviendo más y más estricto y desde la versión 8.0, un intento de usar cualquier operador aritmético/bitwise en arrays, objetos o recursos termina con TypeError. La excepción son las adiciones de arrays.

// arithmetic and bitwise operators
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Comparaciones más sanas entre cadenas y números

O haz que el operador suelto vuelva a ser genial.

Parecería que ya no hay sitio para el operador suelto ==, que es sólo una errata al escribir ===, pero este cambio lo devuelve al mapa de nuevo. Si ya lo tenemos, que se comporte razonablemente. Como resultado de la anterior comparación “irrazonable”, por ejemplo, in_array() podría trollearte desagradablemente:

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

dump(in_array($value, $validValues));
// surprisingly returned true
// since PHP 8.0 returns false

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

Comparison Before 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 una ruptura de BC en el núcleo mismo de la lengua, que ha sido aprobada sin ninguna resistencia. Y eso es bueno. JavaScript podría ser muy celoso en este sentido.

Informe de errores

Muchas funciones internas ahora disparan TypeError y ValueError en lugar de advertencias que eran fáciles de ignorar. Se han reclasificado varias advertencias del kernel. El operador de cierre @ ahora no silencia los errores fatales. Y PDO lanza excepciones por defecto.

Nette siempre intentó solucionar estas cosas de alguna manera. Tracy modificó el comportamiento del operador shutup, Database cambió el comportamiento de PDO, Utils contiene el reemplazo de funciones estándar, que lanzan excepciones en lugar de advertencias fáciles de pasar por alto, etc. Es agradable ver que la dirección estricta que Nette tiene en su ADN se convierte en la dirección nativa del lenguaje.

Incrementos negativos de clave de array

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

¿Cuál será la clave del segundo elemento? Antes era 0, since PHP 8 it’s -4.

Coma final

El último lugar en el que la coma final no podía estar era la definición de los argumentos de las funciones. Esto es cosa del pasado:

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

$object::class

La constante mágica ::class también funciona con los objetos $object::class, sustituyendo completamente a la función get_class().

Atrapar excepciones sólo por tipo

Y por último: no es necesario especificar una variable para la excepción en la cláusula catch:

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

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

En las próximas partes, veremos las principales novedades en cuanto a tipos de datos, mostraremos qué son los atributos, qué nuevas funciones y clases han aparecido en PHP e introduciremos el compilador Just in Time.