PHP 8.0 : Aperçu complet des nouveautés (1/4)

il y a 3 ans de David Grudl  

La version 8.0 de PHP sort en ce moment même. Elle est pleine de nouveautés comme aucune autre version auparavant. Son introduction méritait quatre articles distincts. Dans le premier, nous allons jeter un coup d'oeil à ce qu'elle apporte au niveau du langage.

Avant de nous plonger dans le PHP, il faut savoir que la version actuelle de Nette est entièrement préparée pour la huitième version. De plus, en guise de cadeau, une Nette 2.4 entièrement compatible a été publiée, donc du point de vue du framework, rien ne vous empêche de l'utiliser.

Arguments nommés

Commençons tout de suite par une bombe, qui pourrait être audacieusement désignée comme un changement de jeu. Les arguments peuvent désormais être transmis aux fonctions et aux méthodes non seulement de manière positionnelle, mais aussi en fonction de leur nom. Ce qui est absolument génial dans le cas où une méthode a vraiment trop de paramètres :

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
	) {
		...
	}
}

Les deux premiers arguments sont passés de manière positionnelle, les autres par leur nom : (les noms doivent suivre les positionnels)

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

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

Avant l'introduction de cette fonctionnalité, il était prévu de créer une nouvelle API pour l'envoi de cookies dans Nette, car le nombre d'arguments pour setCookie() a vraiment augmenté et la notation positionnelle était déroutante. Ce n'est plus nécessaire, car les arguments nommés sont dans ce cas l'API la plus pratique. L'IDE les indiquera et il y a une sécurité de type.

Ils conviennent parfaitement même pour expliquer des arguments logiques, où leur utilisation n'est pas nécessaire, mais un simple true ou false ne suffit pas :

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

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

Les noms des arguments font désormais partie de l'API publique. Il n'est plus possible de les modifier à volonté. Pour cette raison, même Nette subit un audit pour déterminer si tous les arguments ont un nom approprié.

Les arguments nommés peuvent également être utilisés en combinaison avec les variadics :

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

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

Le tableau $args peut maintenant contenir même des clés non numériques, ce qui est en quelque sorte une rupture BC. Il en va de même pour le comportement de call_user_func_array($func, $args), où les clés du tableau $args jouent désormais un rôle beaucoup plus important. Au contraire, les fonctions de la famille func_*() sont protégées des arguments nommés.

Les arguments nommés sont étroitement liés au fait que l'opérateur splat ... peut maintenant développer les tableaux associatifs :

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

Étonnamment, il ne fonctionne pas à l'intérieur des tableaux pour le moment :

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

La combinaison d'arguments nommés et de variadics permet d'avoir enfin une syntaxe fixe, par exemple pour la méthode link(), à laquelle nous pouvons passer des arguments nommés ainsi que des arguments positionnels :

// 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 syntaxe des arguments nommés est beaucoup plus sexy que l'écriture de tableaux, aussi Latte l'a immédiatement adoptée, où elle peut être utilisée, par exemple, dans les balises {include} et {link}:

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

Nous reviendrons sur les arguments nommés dans la troisième partie de la série, en relation avec les attributs.

Une expression peut lancer une exception

Lancer une exception est maintenant une expression. Vous pouvez l'envelopper dans des parenthèses et l'ajouter à une condition if. Hmmm, cela ne semble pas très pratique. Cependant, ceci est beaucoup plus intéressant :

// 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');

Les fonctions flèches ne pouvant jusqu'à présent contenir qu'une seule expression, elles peuvent désormais lancer des exceptions grâce à cette fonctionnalité :

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

Expression de la correspondance

L'instruction switch-case présente deux inconvénients majeurs :

  • elle utilise une comparaison non stricte == au lieu de ===
  • vous devez faire attention à ne pas oublier break par erreur

Pour cette raison, PHP propose une alternative sous la forme d'une nouvelle expression match, qui utilise une comparaison stricte et, inversement, n'utilise pas break.

switch Exemple de code :

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;
}

Et le même (seulement avec une comparaison stricte) écrit en utilisant match:

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

Notez que match n'est pas une structure de contrôle comme switch, mais une expression. Dans l'exemple, nous assignons sa valeur résultante à une variable. En même temps, les “options” individuelles sont également des expressions, il n'est donc pas possible d'écrire plusieurs étapes, comme dans le cas de switch.

En cas d'absence de correspondance (et il n'y a pas de clause par défaut), l'exception UnhandledMatchError est levée.

À propos, il existe également des balises {switch}, {case} et {default} dans Latte. Leur fonction correspond exactement à la nouvelle match. Elles utilisent la comparaison stricte, ne nécessitent pas break et il est possible de spécifier plusieurs valeurs séparées par des virgules dans case.

Opérateur Nullsafe

Le chaînage optionnel vous permet d'écrire une expression dont l'évaluation s'arrête si elle rencontre null. C'est grâce au nouvel opérateur ?->. Il remplace une grande partie du code qui, autrement, devrait vérifier de manière répétée la présence de null :

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

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

Pourquoi “approximativement” ? Parce qu'en réalité, l'expression est évaluée de manière plus ingénieuse afin qu'aucune étape ne soit répétée. Par exemple, $user->getAddress() n'est appelé qu'une seule fois, de sorte que le problème causé par la méthode renvoyant quelque chose de différent la première et la deuxième fois ne peut être rencontré.

Cette fonctionnalité a été apportée par “Latte” il y a un an:https://blog.nette.org/…om-functions. Maintenant, c'est PHP lui-même qui l'adopte. Super.

Promotion des propriétés des constructeurs

Un sucre syntaxique qui évite d'écrire le type deux fois et la variable quatre fois. Dommage qu'il ne soit pas arrivé à une époque où nous n'avions pas d'IDE aussi intelligents qui l'écrivent aujourd'hui pour nous 🙂

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,
	) {}
}

Il fonctionne avec Nette DI, vous pouvez commencer à l'utiliser.

Vérification plus stricte des types d'opérateurs arithmétiques et binaires

Ce qui aidait autrefois les langages de script dynamiques à s'imposer est devenu leur point faible. Autrefois, PHP s'est débarrassé des “guillemets magiques”, de l'enregistrement des variables globales et aujourd'hui, le comportement détendu est remplacé par la rigueur. Le temps où, en PHP, vous pouviez ajouter, multiplier, etc. presque tous les types de données pour lesquels cela n'avait aucun sens, est révolu. Depuis la version 7.0, PHP devient de plus en plus strict et depuis la version 8.0, toute tentative d'utiliser des opérateurs arithmétiques ou binaires sur des tableaux, des objets ou des ressources se termine par une TypeError. L'exception est l'ajout de tableaux.

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

Des comparaisons plus saines entre chaînes et nombres

Ou rendez l'opérateur libre à nouveau génial.

Il semblerait qu'il n'y ait plus de place pour l'opérateur loose ==, qu'il s'agisse simplement d'une faute de frappe lors de l'écriture de ===, mais ce changement le remet sur le devant de la scène. Si nous l'avons déjà, laissons-le se comporter raisonnablement. À la suite de la comparaison “déraisonnable” précédente, par exemple, in_array() pourrait vous troller désagréablement :

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

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

Le changement de comportement de == concerne la comparaison de nombres et de chaînes “numériques” et est présenté dans le tableau suivant :

Comparaison Avant PHP 8.0
0 == "0" vrai vrai
0 == "0.0" vrai vrai
0 == "foo" vrai faux
0 == "" vrai faux
42 == " 42" vrai vrai
42 == "42 " vrai vrai
42 == "42foo" vrai faux
42 == "abc42" vrai faux
"42" == " 42" vrai vrai
"42" == "42 " faux vrai

De manière surprenante, c'est une rupture de la CB au cœur même de la langue, qui a été approuvée sans aucune résistance. Et c'est une bonne chose. JavaScript pourrait être très jaloux à cet égard.

Rapports d'erreurs

De nombreuses fonctions internes déclenchent maintenant TypeError et ValueError au lieu d'avertissements qui étaient faciles à ignorer. Un certain nombre d'avertissements du noyau ont été reclassés. L'opérateur d'arrêt @ ne tait plus les erreurs fatales. Et PDO lance des exceptions par défaut.

Nette a toujours essayé de résoudre ces choses d'une manière ou d'une autre. Tracy a modifié le comportement de l'opérateur shutup, Database a changé le comportement de PDO, Utils contient des remplacements pour les fonctions standards, qui lancent des exceptions au lieu d'avertissements faciles à manquer, etc. C'est agréable de voir que la direction stricte que Nette a dans son ADN devient la direction native du langage.

Incrémentation des clés de tableau négatives

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

Quelle sera la clé du deuxième élément ? Avant, c'était 0, since PHP 8 it’s -4.

Virgule de fin

Le dernier endroit où la virgule de fin ne pouvait pas être, était la définition des arguments de fonction. C'est une chose du passé :

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

$object::class

La constante magique ::class fonctionne également avec les objets $object::class, remplaçant complètement la fonction get_class().

Attraper les exceptions uniquement par type

Et enfin : il n'est pas nécessaire de spécifier une variable pour l'exception dans la clause catch :

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

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

Dans les prochaines parties, nous verrons les innovations majeures concernant les types de données, nous montrerons ce que sont les attributs, quelles nouvelles fonctions et classes sont apparues en PHP et nous présenterons le compilateur Just in Time.