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

il y a 4 ans par David Grudl  

La version 8.0 de PHP est sortie. Elle est tellement pleine de nouveautés qu'aucune version précédente ne l'a été. Leur présentation a nécessité quatre articles distincts. Dans ce premier, nous allons examiner les nouveautés du langage.

Avant de plonger dans PHP, sachez que la version actuelle de Nette est entièrement prête pour la version 8. De plus, en cadeau, Nette 2.4 est également sorti, entièrement compatible avec elle, donc du point de vue du framework, rien ne vous empêche de commencer à utiliser la nouvelle version.

Arguments nommés

Et nous commençons directement par une bombe, qui peut être qualifiée de “game changer”. Il est désormais possible de passer des arguments aux fonctions et méthodes non seulement de manière positionnelle, mais aussi par leur nom. Ce qui est absolument génial dans le cas où une méthode a vraiment beaucoup 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
	) {
		...
	}
}

Nous passons les deux premiers arguments de manière positionnelle, les suivants par leur nom : (les arguments nommés doivent toujours suivre les arguments positionnels)

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

// au lieu de la folie
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Avant l'arrivée de cette fonctionnalité, il était prévu de créer une nouvelle API dans Nette pour l'envoi de cookies, car le nombre de paramètres de setCookie() avait vraiment augmenté et la notation positionnelle était peu claire. Ce n'est plus nécessaire maintenant, car les arguments nommés sont dans ce cas l'API la plus pratique. L'IDE les suggérera et ils auront un contrôle de type.

Ils sont également parfaits pour clarifier les paramètres logiques, où leur utilisation n'est certes pas nécessaire, mais true ou false seuls ne disent pas grand-chose :

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

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

Les noms des paramètres font désormais partie de l'API publique. Il n'est plus possible de les modifier arbitrairement comme auparavant. C'est pourquoi Nette subit également un audit pour vérifier si tous les paramètres ont une dénomination appropriée.

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 contiendra ['b' => 2, 'c' => 3]

Le tableau $args peut donc désormais contenir également des clés non numériques, ce qui constitue une certaine rupture de compatibilité (BC break). Il en va de même pour le comportement de la fonction call_user_func_array($func, $args), où les clés du tableau $args jouent désormais un rôle significatif. En revanche, les fonctions de la famille func_*() sont isolées des arguments nommés.

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

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

Curieusement, cela ne fonctionne pas encore à l'intérieur des tableaux :

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

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

// avant
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // devait être un tableau

// maintenant
$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 paramètres nommés dans la troisième partie en relation avec les attributs.

Une expression peut lever une exception

Lever une exception est désormais une expression. Vous pouvez par exemple l'envelopper dans des parenthèses et l'ajouter à une condition if. Hmmm, cela ne semble pas très pratique. Mais ceci est déjà plus intéressant :

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


// maintenant, quand throw est une expression
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Comme les fonctions fléchées ne peuvent jusqu'à présent contenir qu'une seule expression, grâce à cette fonctionnalité, elles peuvent lever des exceptions :

// seulement une seule expression
$fn = fn() => throw new \Exception('oops');

Expressions Match

La construction switch-case a deux grands défauts :

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

PHP propose donc une alternative sous la forme de la nouvelle construction match, qui utilise une comparaison stricte et, inversement, n'utilise pas break.

Exemple de code 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;
}

Et la même chose (seulement avec une comparaison stricte) écrite 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.

S'il n'y a pas de correspondance avec l'une des options (et qu'il n'y a pas de clause default), une exception UnhandledMatchError est levée.

D'ailleurs, dans Latte, il existe également les balises {switch}, {case} et {default}. Leur fonctionnement correspond exactement au nouveau match. Elles utilisent une comparaison stricte, ne nécessitent pas break et dans case, il est possible d'indiquer plusieurs valeurs séparées par des virgules.

Opérateur Nullsafe

Le chaînage optionnel (optional chaining) permet d'écrire une expression dont l'évaluation s'arrête si elle rencontre null. Et ce, grâce au nouvel opérateur ?->. Il remplace beaucoup de code qui vérifierait sinon de manière répétée la présence de null :

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

// signifie environ
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Pourquoi “signifie environ” ? Parce qu'en réalité, l'expression est évaluée de manière plus ingénieuse et aucune étape n'est répétée. Par exemple, $user->getAddress() n'est appelé qu'une seule fois, donc il ne peut pas y avoir de problème causé par le fait que la méthode renverrait quelque chose de différent la première et la deuxième fois.

Cette nouveauté fraîche a été introduite par Latte il y a un an. Elle arrive maintenant dans PHP lui-même. Parfait.

Promotion des propriétés du constructeur

Du sucre syntaxique qui évite d'écrire deux fois le type et quatre fois la variable. Dommage qu'il ne soit pas arrivé à l'é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,
	) {}
}

Cela fonctionne avec Nette DI, vous pouvez commencer à l'utiliser immédiatement.

Comportement strict des opérateurs arithmétiques et bitwise

Ce qui a autrefois propulsé les langages de script dynamiques au sommet est devenu avec le temps leur point le plus faible. PHP s'est autrefois débarrassé des “magic quotes”, de l'enregistrement des variables globales, et maintenant le comportement laxiste est remplacé par la rigueur. L'époque où vous pouviez en PHP additionner, multiplier, etc. presque tous les types de données pour lesquels cela n'avait absolument aucun sens est révolue depuis longtemps. À partir de la version 7.0, PHP est de plus en plus strict, et à partir de la version 8.0, toute tentative d'utilisation d'un opérateur arithmétique/bitwise sur des tableaux, des objets ou des ressources se termine par une TypeError. L'exception est l'addition de tableaux.

// opérateurs arithmétiques et bitwise
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Comparaison plus raisonnable des chaînes de caractères et des nombres

Ou : “make loose operator great again”.

Il semblerait que l'opérateur loose == n'ait plus sa place, qu'il ne s'agisse que d'une faute de frappe lors de l'écriture de ===, mais ce changement le remet sur la carte. Puisque nous l'avons, qu'il se comporte raisonnablement. La conséquence de la comparaison “déraisonnable” précédente était par exemple le comportement de in_array(), qui pouvait vous jouer un mauvais tour :

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

dump(in_array($value, $validValues));
// retournait true de manière surprenante
// à partir de PHP 8.0, retourne false

Le changement de comportement de == concerne la comparaison des nombres et des chaînes “numériques” et est illustré par le tableau suivant :

Comparaison Avant 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

Il est surprenant qu'il s'agisse d'une rupture de compatibilité (BC break) dans le fondement même du langage, qui a été approuvée sans aucune opposition. Et c'est une bonne chose. JavaScript pourrait beaucoup envier cela.

Rapport d'erreurs

De nombreuses fonctions internes lèvent désormais des TypeError et ValueError au lieu d'avertissements qui pouvaient être facilement négligés. De nombreux avertissements du noyau ont été reclassifiés. L'opérateur shutup @ ne masque plus les erreurs fatales. Et PDO lève des exceptions en mode par défaut.

Nette a toujours essayé de résoudre ces problèmes d'une manière ou d'une autre. Tracy modifiait le comportement de l'opérateur shutup, Database changeait le comportement de PDO, Utils contient des remplacements de fonctions standard qui lèvent des exceptions au lieu d'avertissements discrets, etc. Il est bon de voir que la direction stricte, qui est dans l'ADN de Nette, devient la direction native du langage.

Tableaux avec index négatif

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

Quelle sera la clé du deuxième élément ? Avant, c'était 0, depuis PHP 8, c'est -4.

Virgule finale

Le dernier endroit où une virgule finale ne pouvait pas se trouver était la définition des paramètres de fonction. C'est désormais du passé :

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

$object::class

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

catch sans variable

Et enfin : dans la clause catch, il n'est pas nécessaire d'indiquer une variable pour l'exception :

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

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

Dans les prochaines parties, nous découvrirons des nouveautés majeures dans les types de données, nous verrons ce que sont les attributs, quelles nouvelles fonctions et classes sont apparues en PHP, et nous présenterons le Just in Time Compiler.