PHP 8 a été officiellement mis à la disposition du public le 26 novembre 2020 !

Cette nouvelle mise à jour majeure apporte tout un tas d’optimisations et de puissantes fonctionnalités au langage et nous sommes heureux de vous présenter les changements les plus intéressants qui nous permettront d’écrire un meilleur code et de construire des applications plus puissantes.

PHP 8 released
Addendum à l’annonce du PHP 8.0

Êtes-vous prêt ? Plongeons dans le vif du sujet !

PHP JIT (Compilation Just in Time)

La fonctionnalité la plus acclamée de PHP 8 est la compilation à la volée (Just In Time). Qu’est-ce que JIT ?

La proposition de RFC décrit JIT comme suit :

« PHP JIT est mis en œuvre comme une partie presque indépendante d’OPcache. Il peut être activé / désactivé au moment de la compilation et de l’exécution de PHP. Lorsqu’il est activé, le code natif des fichiers PHP est stocké dans une région supplémentaire de la mémoire partagée d’OPcache et op_array→opcodes[].handler(s) conserve les pointeurs vers les points d’entrée du code JIT-ed ».

Alors, comment en sommes-nous arrivés à JIT et quelle est la différence entre JIT et OPcache ?

Pour mieux comprendre ce qu’est JIT pour PHP, examinons rapidement comment PHP s’exécute du code source au résultat final.

L’exécution de PHP est un processus en 4 étapes :

  • Lexing/Tokenizing : Tout d’abord, l’interprèteur lit le code PHP et construit un ensemble de jetons.
  • Parsing : L’interprèteur vérifie si le script correspond aux règles syntaxiques et utilise des jetons pour construire un arbre syntaxique abstrait (Abstract Syntax Tree ou AST), qui est une représentation hiérarchique de la structure du code source.
  • Compilation : L’interprèteur parcourt l’arbre et traduit les nœuds AST en opcodes Zend de bas niveau, qui sont des identificateurs numériques déterminant le type d’instruction exécuté par la VM Zend.
  • Interprétation : Les opcodes sont interprétés et exécutés sur la VM Zend.

L’image suivante montre une représentation visuelle du processus d’exécution de base de PHP.

Processus d'exécution de base de PHP
Processus d’exécution de base de PHP

Alors, comment OPcache rend-il PHP plus rapide ? Et quels sont les changements dans le processus d’exécution avec JIT ?

L’extension OPcache

PHP est un langage interprété. Cela signifie que lorsqu’un script PHP est exécuté, l’interprèteur analyse, compile et exécute le code à chaque requête. Cela peut entraîner une perte de ressources CPU et du temps supplémentaire.

C’est là que l’extension OPcache entre en jeu :

« OPcache améliore les performances de PHP en stockant le bytecode des scripts pré-compilés dans la mémoire partagée, ce qui évite à PHP de charger et d’analyser les scripts à chaque requête. »

Lorsque OPcache est activé, l’interpréteur PHP passe par les quatre étapes mentionnées ci-dessus uniquement la première fois que le script est exécuté. Comme les bytecodes PHP sont stockés dans la mémoire partagée, ils sont immédiatement disponibles en tant que représentation intermédiaire de bas niveau et peuvent être exécutés immédiatement sur la VM Zend.

Processus d'exécution PHP avec OPcache activé
Processus d’exécution PHP avec OPcache activé

À partir de PHP 5.5, l’extension Zend OPcache est disponible par défaut et vous pouvez vérifier si vous l’avez correctement configurée en appelant simplement phpinfo() depuis un script sur votre serveur ou en consultant votre fichier php.ini (voir les réglages de configuration d’OPcache).

Lecture suggérée : Comment améliorer la limite de mémoire PHP dans WordPress.

Section Zend OPcache dans une page phpinfo
Section Zend OPcache dans une page phpinfo

Préchargement

OPcache a été récemment amélioré avec la mise en œuvre du préchargement, une nouvelle fonctionnalité d’OPcache ajoutée avec PHP 7.4. Le préchargement permet de stocker un ensemble spécifique de scripts dans la mémoire d’OPcache « avant l’exécution de tout code d’application », mais il n’apporte pas d’amélioration tangible des performances des applications web typiques.

Vous pouvez en savoir plus sur le préchargement dans notre introduction à PHP 7.4.

Avec JIT, PHP fait un pas en avant.

JIT – La compilation à la volée

Même si les opcodes se présentent sous la forme d’une représentation intermédiaire de bas niveau, ils doivent encore être compilés en code machine. JIT « n’introduit pas de forme IR (Intermediate Representation) supplémentaire », mais utilise DynASM (Assembleur dynamique pour les moteurs de génération de code) pour générer du code natif directement à partir du byte-code PHP.

En bref, JIT traduit les parties chaudes du code intermédiaire en code machine. En contournant la compilation, il serait en mesure d’apporter des améliorations considérables en termes de performances et d’utilisation de la mémoire.

Zeev Surasky, co-auteur de la proposition de PHP JIT, montre à quel point les calculs seraient plus rapides avec JIT :

Mais, JIT améliorerait-il efficacement les performances de WordPress ?

JIT pour les applications web en direct

Selon la RFC de JIT, la mise en œuvre du compilateur « just in time » devrait améliorer les performances de PHP. Mais connaîtrions-nous vraiment de telles améliorations dans des applications de la vie réelle comme WordPress ?

Les premiers tests montrent que JIT permettrait de faire fonctionner beaucoup plus rapidement les charges de travail gourmandes en CPU, mais la RFC met en garde :

« … comme les tentatives précédentes – cela ne semble pas actuellement améliorer de manière significative les applications de la vie réelle comme WordPress (avec opcache.jit=1235 326 req/sec vs 315 req/sec).

Il est prévu de fournir un effort supplémentaire, en améliorant JIT pour les applications de la vie réelle, en utilisant le profilage et les optimisations spéculatives ».

Avec JIT activé, le code ne serait pas exécuté par la VM Zend, mais par l’unité centrale elle-même, ce qui améliorerait la vitesse de calcul. Les applications web comme WordPress reposent également sur d’autres facteurs comme le TTFB, l’optimisation des bases de données, les requêtes HTTP, etc.

PHP 8 performance diagram
Contribution relative de l’ECE aux performances du PHP 8 (Source de l’image Addendum à l’annonce du PHP 8.0)

Ainsi, en ce qui concerne WordPress et les applications similaires, il ne faut pas s’attendre à une grande augmentation de la vitesse d’exécution de PHP. Néanmoins, JIT pourrait apporter plusieurs avantages aux développeurs.

Selon Nikita Popov :

« Les avantages du compilateur JIT sont en gros (et comme déjà souligné dans le RFC) :

  • Des performances nettement meilleures pour le code numérique.
  • Légèrement plus performant pour le code PHP d’application web « typique ».
  • La possibilité de faire passer davantage de code de C à PHP, car PHP sera désormais suffisamment rapide ».

Ainsi, alors que JIT n’apportera guère d’améliorations considérables aux performances de WordPress, il fera passer PHP au niveau supérieur, en en faisant un langage dans lequel de nombreuses fonctions pourraient désormais être écrites directement.

L’inconvénient, cependant, serait la plus grande complexité qui peut entraîner une augmentation des coûts de maintenance, de stabilité et de débogage. Selon Dmitry Stogov :

« JIT est extrêmement simple, mais de toute façon il augmente le niveau de complexité du PHP, le risque de nouveaux types de bugs et le coût du développement et de la maintenance. »

La proposition d’inclure JIT dans PHP 8 a été adoptée par 50 voix contre 2.

Améliorations et nouvelles fonctionnalités de PHP 8

En dehors de JIT, nous pouvons nous attendre à de nombreuses fonctionnalités et améliorations avec PHP 8. La liste suivante est notre sélection des ajouts et changements à venir qui devraient rendre PHP plus fiable et plus efficace.

Constructor Property Promotion

Suite à une discussion en cours sur la façon d’améliorer l’ergonomie des objets en PHP, le RFC « Constructor Property Promotion » propose une nouvelle syntaxe plus concise qui simplifiera la déclaration de propriété, la rendant plus courte et moins redondante.

Cette proposition ne concerne que les “promoted parameters” ou paramètres promus, c’est-à-dire les paramètres de méthode préfixés par des mots-clés de visibilité publique, protégée et privée.

Actuellement, toutes les propriétés doivent être répétées plusieurs fois (au moins quatre fois) avant que nous puissions les utiliser avec des objets. Considérons l’exemple suivant, tiré du RFC :

class Point {
    public int $x;
    public int $y;
    public int $z;

    public function __construct(
        int $x = 0,
        int $y = 0,
        int $z = 0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

Selon Nikita Popov, l’auteure du RFC, nous devons écrire le nom de la propriété au moins quatre fois à trois endroits différents : la déclaration de propriété, les paramètres du constructeur et l’affectation de la propriété. Cette syntaxe n’est pas particulièrement utilisable, surtout dans les classes ayant un bon nombre de propriétés et des noms plus descriptifs.

Ce RFC propose de fusionner le constructeur et la définition des paramètres. Ainsi, à partir de PHP 8, nous disposons d’une façon plus utilisable de déclarer les paramètres et le code vu ci-dessus peut changer comme indiqué ci-dessous :

class Point {
    public function __construct(
        public int $x = 0,
        public int $y = 0,
        public int $z = 0,
    ) {}
}

Et c’est tout. Nous avons donc une nouvelle façon de promouvoir les propriétés qui est plus courte, plus lisible et moins sujette aux erreurs. Selon Nikita :

C’est une simple transformation syntaxique que nous faisons. Mais cela réduit la quantité de code passe-partout que vous devez écrire pour les objets de valeur en particulier…

La déclaration de propriété est transformée comme nous avions explicitement déclaré ces propriétés et nous pouvons utiliser l’API Reflection pour introspecter les définitions de propriété avant l’exécution (voir Desugaring) :

La réflexion (et autres mécanismes d’introspection) permettra d’observer l’état après le “desugaring”. Cela signifie que les propriétés promues apparaîtront de la même manière que les propriétés explicitement déclarées, et que les arguments du constructeur promu apparaîtront comme des arguments du constructeur ordinaire.

// before desugaring
class Point {
    public function __construct(public int $x = 0) {}
}

// after desugaring
class Point {
    public int $x;

    public function __construct(int $x = 0) {
        $this->x = $x;
    }
}

Inheritance

Nous n’avons aucune limite à l’utilisation de l’héritage en conjonction avec des paramètres promus. De toute façon, il n’y a pas de relation particulière entre les constructeurs de classes de parents et d’enfants. Selon Nikita :

Habituellement, nous disons que les méthodes doivent toujours être compatibles avec la méthode mère. […] mais cette règle ne s’applique pas au constructeur. Ainsi, le constructeur appartient réellement à une seule classe, et les constructeurs entre la classe parent et la classe enfant n’ont pas à être compatibles d’une quelconque manière.

Voici un exemple:

class Test {
    public function __construct(
        public int $x = 0
    ) {}
}

class Child extends Test {
    public function __construct(
        $x, 
        public int $y = 0,
        public int $z = 0,
    ) {
        parent::__construct($x);
    }
}

Ce qui n’est pas autorisé avec les propriétés promues

Les propriétés promues sont autorisées dans les constructeurs et les traits non abstraits, mais il y a plusieurs limitations qui méritent d’être mentionnées ici.

Constructeurs abstraits

Les propriétés promues ne sont pas autorisées dans les classes et les interfaces abstraites :

abstract class Test {
    // Error: Abstract constructor.
    abstract public function __construct(private $x);
}
 
interface Test {
    // Error: Abstract constructor.
    public function __construct(private $x);
}
Nullabilité

L’une des contraintes les plus notables est liée à l’annulabilité. Auparavant, lorsque nous utilisions un type qui n’était pas explicitement annulable, mais avec une valeur par défaut nulle, le type était implicitement annulable. Mais avec les types de propriété, nous n’avons pas ce comportement implicite car les paramètres promus nécessitent une déclaration de propriété, et le type annulable doit être explicitement déclaré. Voir l’exemple suivant de la RFC :

class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}

    // Correct: Make the type explicitly nullable instead
    public function __construct(public ?Type $prop = null) {}
}
Callable Type

Comme le type callable n’est pas pris en charge pour les propriétés, nous ne sommes pas autorisés à utiliser le type callable dans les propriétés promues :

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}
Le mot-clé var n’est pas autorisé

Seul un mot-clé de visibilité peut être utilisé avec les paramètres promus, donc la déclaration des propriétés du constructeur avec le mot-clé var n’est pas autorisée (voir l’exemple suivant du RFC) :

class Test {
    // Error: "var" keyword is not supported.
    public function __construct(var $prop) {}
}
Aucune duplication autorisé

Nous pouvons combiner des propriétés promues et des propriétés explicites dans la même classe, mais les propriétés ne peuvent pas être déclarées deux fois :

class Test {
    public string $prop;
    public int $explicitProp;

    // Correct
    public function __construct(public int $promotedProp, int $arg) {
        $this->explicitProp = $arg;
    }

    // Error: Redeclaration of property.
    public function __construct(public string $prop) {}
}
Les paramètres variadiques ne sont pas autorisés

La raison en est que le type déclaré est différent du paramètre variadique, qui est en fait un tableau :

class Test {
    // Error: Variadic parameter.
    public function __construct(public string ...$strings) {}
}

Lectures complémentaires

Pour en savoir plus sur Costructor Property Promotion, écoutez cette interview de Nikita Popov. Pour un aperçu détaillé de l’ergonomie des objets en PHP, voir ce post et l’interview suivante avec Larry Garfield.

Validation pour les méthodes de traits abstraits

Les traits sont définis comme « un mécanisme de réutilisation de code dans des langages à héritage unique tels que PHP ». Ils sont généralement utilisés pour déclarer des méthodes qui peuvent être utilisées dans plusieurs classes.

Un trait peut également contenir des méthodes abstraites. Ces méthodes déclarent simplement la signature de la méthode, mais l’implémentation de la méthode doit être faite au sein de la classe qui utilise le trait.

Selon le manuel de PHP,

« Les traits prennent en charge l’utilisation de méthodes abstraites afin d’imposer des exigences à la classe exposante ».

Cela signifie également que les signatures des méthodes doivent correspondre. En d’autres termes, le type et le nombre d’arguments nécessaires doivent être les mêmes.

Quoi qu’il en soit, selon Nikita Popov, auteur de la RFC, la validation des signatures n’est actuellement imposée que de manière ponctuelle :

  • Elle n’est pas imposée dans le cas le plus courant, où l’application de la méthode est fournie par l’utilisation de la classe  : https://3v4l.org/SeVK3
  • Elle est imposée si l’implémentation provient d’une classe parente : https://3v4l.org/4VCIp
  • Elle est imposée si l’implémentation provient d’une classe enfant : https://3v4l.org/q7Bq2

L’exemple suivant de Nikita concerne le premier cas (signature non imposée) :

trait T {
	abstract public function test(int $x);
}
 
class C {
	use T;

	// Allowed, but shouldn't be due to invalid type.
	public function test(string $x) {}
}

Cela étant dit, cette RFC propose de toujours lancer une erreur fatale si la méthode de mise en œuvre n’est pas compatible avec la méthode des traits abstraits, quelle que soit son origine :

Fatal error: Declaration of C::test(string $x) must be compatible with T::test(int $x) in /path/to/your/test.php on line 10

Cette RFC a été approuvée à l’unanimité.

Signatures de méthodes incompatibles

En PHP, les erreurs d’héritage dûes à des signatures de méthodes incompatibles déclenchent soit une erreur fatale, soit un avertissement selon la cause de l’erreur.

Si une classe implémente une interface, les signatures de méthodes incompatibles entraînent une erreur fatale. Selon la documentation sur les interfaces objet :

« La classe qui implémente l’interface doit utiliser une signature de méthode qui est compatible avec le LSP (Liskov Substitution Principle). Ne pas le faire entraînera une erreur fatale ».

Voici un exemple d’erreur d’héritage avec une interface :

interface I {
	public function method(array $a);
}
class C implements I {
	public function method(int $a) {}
}

Dans PHP 7.4, le code ci-dessus entraînerait l’erreur suivante :

Fatal error: Declaration of C::method(int $a) must be compatible with I::method(array $a) in /path/to/your/test.php on line 7

Une fonction dans une classe enfant avec une signature incompatible lancerait un avertissement. Voir le code suivant de la RFC :

class C1 {
	public function method(array $a) {}
}
class C2 extends C1 {
	public function method(int $a) {}
}

Dans PHP 7.4, le code ci-dessus lancerait simplement un avertissement :

Warning: Declaration of C2::method(int $a) should be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Or, cette RFC propose de toujours lancer une erreur fatale pour les signatures de méthodes incompatibles. Avec PHP 8, le code que nous avons vu plus haut déclencherait ce qui suit :

Fatal error: Declaration of C2::method(int $a) must be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Tableaux commençant par un index négatif

En PHP, si un tableau commence par un index négatif (start_index < 0), les indices suivants partiront de 0 (plus d’informations à ce sujet dans la documentation de array_fill). Regardez l’exemple suivant :

$a = array_fill(-5, 4, true);
var_dump($a);

Dans PHP 7.4, le résultat serait le suivant :

array(4) {
	[-5]=>
	bool(true)
	[0]=>
	bool(true)
	[1]=>
	bool(true)
	[2]=>
	bool(true)
}

Maintenant, cette RFC propose de changer les choses de manière à ce que le deuxième indice soit start_index + 1, quelle que soit la valeur de start_index.

Dans PHP 8, le code ci-dessus donnerait le tableau suivant :

array(4) {
	[-5]=>
	bool(true)
	[-4]=>
	bool(true)
	[-3]=>
	bool(true)
	[-2]=>
	bool(true)
}

Avec PHP 8, les tableaux commençant par un index négatif modifient leur comportement. Pour en savoir plus sur les incompatibilités ascendantes dans la RFC.

Types d’union 2.0

Les types d’union (Union Types) acceptent des valeurs qui peuvent être de différents types. Actuellement, PHP ne fournit pas de support pour les types d’union, à l’exception de la syntaxe ?Type et du type spécial iterable.

Avant PHP 8, les types d’Union ne pouvaient être spécifiés que dans les annotations phpdoc, comme le montre l’exemple suivant de la RFC :

class Number {
	/**
	 * @var int|float $number
	 */
	private $number;

	/**
	 * @param int|float $number
	 */
	public function setNumber($number) {
		$this->number = $number;
	}

	/**
	 * @return int|float
	 */
	public function getNumber() {
		return $this->number;
	}
}

Maintenant, la RFC des types d’Union 2.0 propose d’ajouter la prise en charge des types d’Union dans les signatures de fonctions, de sorte que nous ne dépendrons plus de la documentation en ligne, mais définirons les types d’Union avec une syntaxe T1|T2|... à la place :

class Number {
	private int|float $number;

	public function setNumber(int|float $number): void {
		$this->number = $number;
	}

	public function getNumber(): int|float {
		return $this->number;
	}
}

Comme l’explique Nikita Popov dans la RFC,

« La prise en charge des types d’Union dans le language nous permet de déplacer davantage d’informations de type du phpdoc vers les signatures de fonction, avec les avantages habituels que cela apporte :

  • Les types sont effectivement imposés, de sorte que les erreurs peuvent être détectées à temps.
  • Parce qu’elles sont imposées, les informations de type sont moins susceptibles de devenir obsolètes ou de manquer les « edge-cases ».
  • Les types sont vérifiés lors de l’héritage, en appliquant le principe de substitution de Liskov.
  • Les types sont disponibles par l’intermédiaire de Reflection.
  • La syntaxe est beaucoup moins passe-partout que celle de phpdoc ».

Les types d’Union prennent en charge tous les types disponibles, avec certaines limitations :

  • Le type void ne pourrait pas faire partie d’une union, car void signifie qu’une fonction ne retourne aucune valeur.
  • Le type null n’est pris en charge que dans les types d’union, mais son utilisation en tant que type autonome n’est pas autorisée.
  • La notation de type nullable (?T) est également autorisée, c’est-à-dire T|null, mais nous ne sommes pas autorisés à inclure la notation ?T dans les types d’union (?T1|T2 n’est pas autorisé et nous devrions utiliser T1|T2|null à la place).
  • Comme de nombreuses fonctions (c’est-à-dire strpos(), strstr(), substr(), etc.) incluent false parmi les types de retour possibles, le pseudo-type false est également pris en charge.

Vous pouvez en savoir plus sur les types d’Union V2 dans la RFC.

Erreurs de type cohérentes pour les fonctions internes

Lors du passage d’un paramètre de type illégal, les fonctions internes et celles définies par l’utilisateur se comportent différemment.

Les fonctions définies par l’utilisateur provoquent une TypeError, mais les fonctions internes se comportent de différentes manières, selon plusieurs conditions. Quoi qu’il en soit, le comportement typique est de lancer un avertissement et de retourner null. Voir l’exemple suivant en PHP 7.4 :

var_dump(strlen(new stdClass));

Il en résulterait l’avertissement suivant :

Warning: strlen() expects parameter 1 to be string, object given in /path/to/your/test.php on line 4
NULL

Si strict_types est activé, ou si les informations sur les arguments spécifient les types, le comportement sera différent. Dans de tels scénarios, l’erreur de type est détectée et entraîne une TypeError.

Cette situation entraînerait un certain nombre de problèmes bien expliqués dans la section des questions de la RFC.

Pour supprimer ces incohérences, cette RFC propose de faire en sorte que les APIs d’analyse des paramètres internes génèrent toujours une ThrowError en cas de non-concordance d’un type de paramètre.

Dans PHP 8, le code ci-dessus provoque l’erreur suivante :

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, object given in /path/to/your/test.php:4
Stack trace:
#0 {main}
  thrown in /path/to/your/test.php on line 4

Expression throw

Dans PHP, throw est une déclaration, il n’est donc pas possible de l’utiliser dans les endroits où seule une expression est autorisée.

Cette RFC propose de convertir la déclaration throw en une expression afin qu’elle puisse être utilisée dans tout contexte où les expressions sont autorisées. Par exemple, les fonctions arrow, l’opérateur coalesce, les opérateurs ternary et elvis operators, etc.

Voyez les exemples suivants de la RFC :

$callable = fn() => throw new Exception();

// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();
 
// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();

Weak Maps

Weak Map est un ensemble de données (objets) dans lequel les clés sont faiblement référencées, ce qui signifie qu’elles ne sont pas empêchées d’être collectées.

PHP 7.4 a ajouté la prise en charge des références faibles comme moyen de conserver une référence à un objet qui n’empêche pas l’objet lui-même d’être détruit. Comme l’a souligné Nikita Popov,

« Les références brutes faibles n’ont qu’une utilité limitée en soi et les weak maps sont beaucoup plus couramment utilisées dans la pratique. Il n’est pas possible de mettre en œuvre une weak map efficace en plus des références faibles de PHP parce que la possibilité d’enregistrer un callback de destruction n’est pas prévue ».

C’est pourquoi cette RFC introduit une classe WeakMap pour créer des objets à utiliser comme clés de weak map qui peuvent être détruites et retirées de la weak map s’il n’y a pas d’autres références à l’objet clé.

Dans les processus de longue durée, cela permettrait d’éviter les fuites de mémoire et d’améliorer les performances. Voir l’exemple suivant de la RFC :

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

Avec PHP 8, le code ci-dessus produirait le résultat suivant (voir le code en action ici) :

object(WeakMap)#1 (1) {
	[0]=>
	array(2) {
		["key"]=>
		object(stdClass)#2 (0) {
		}
		["value"]=>
		int(42)
	}
}

Si vous désactivez l’objet, la clé est automatiquement retirée de la weak map :

unset($obj);
var_dump($map);

Le résultat serait maintenant le suivant :

object(WeakMap)#1 (0) {
}

Pour un examen plus approfondi des weaks maps, voir la RFC. La proposition a été approuvée à l’unanimité.

Virgule de fin dans la liste des paramètres

Les virgules de fin de ligne sont des virgules annexées à des listes d’éléments dans différents contextes. PHP 7.2 a introduit les virgules de fin de liste dans la syntaxe des listes, PHP 7.3 a introduit les virgules de fin de liste dans les appels de fonction.

PHP 8 introduit désormais des virgules de fin de ligne dans les listes de paramètres avec les fonctions, les méthodes et les fermetures, comme le montre l’exemple suivant :

class Foo {
	public function __construct(
		string $x,
		int $y,
		float $z, // trailing comma
	) {
		// do something
	}
}

Cette proposition a été adoptée par 58 voix contre 1.

Allow :: Syntaxe de classe sur les objets

Afin d’obtenir le nom d’une classe, nous pouvons utiliser la syntaxe Foo\Bar::class. Cette RFC propose d’étendre la même syntaxe aux objets, de sorte qu’il est maintenant possible de récupérer le nom de la classe d’un objet donné, comme le montre l’exemple ci-dessous :

$object = new stdClass;
var_dump($object::class); // "stdClass"
 
$object = null;
var_dump($object::class); // TypeError

Avec PHP 8, $object::class fournit le même résultat que get_class($object). Si $object n’est pas un objet, il lance une exception TypeError.

Cette proposition a été approuvée à l’unanimité.

Attributs v2

Les attributs, également appelés annotations, sont une forme de métadonnées structurées qui peuvent être utilisées pour spécifier les propriétés des objets, des éléments ou des fichiers.

Jusqu’à la version 7.4 de PHP, les doc-comments étaient le seul moyen d’ajouter des métadonnées aux déclarations de classes, fonctions, etc. Maintenant, la RFC Attributs v2 introduit des attributs pour PHP en les définissant comme une forme de métadonnées structurées et syntaxiques qui peuvent être ajoutées aux déclarations de classes, propriétés, fonctions, méthodes, paramètres et constantes.

Les attributs sont ajoutés avant les déclarations auxquelles ils se réfèrent. Voir les exemples suivants de la RFC :

<<ExampleAttribute>>
class Foo
{
	<<ExampleAttribute>>
	public const FOO = 'foo';

	<<ExampleAttribute>>
	public $x;

	<<ExampleAttribute>>
	public function foo(<<ExampleAttribute>> $bar) { }
}

$object = new <<ExampleAttribute>> class () { };

<<ExampleAttribute>>
function f1() { }

$f2 = <<ExampleAttribute>> function () { };

$f3 = <<ExampleAttribute>> fn () => 1;

Les attributs peuvent être ajoutés avant ou après un commentaire de doc-block :

<<ExampleAttribute>>
/** docblock */
<<AnotherExampleAttribute>>
function foo() {}

Chaque déclaration peut avoir un ou plusieurs attributs et chaque attribut peut avoir une ou plusieurs valeurs associées :

<<WithoutArgument>>
<<SingleArgument(0)>>
<<FewArguments('Hello', 'World')>>
function foo() {}

Voir la RFC pour un aperçu plus détaillé des attributs PHP, des cas d’utilisation et de la syntaxe alternative. Notez que la version 2 des attributs est actuellement en cours d’implémentation.

Arguments nommés

Les arguments nommés fournissent une nouvelle façon de passer des arguments à une fonction dans le PHP :

Les arguments nommés permettent de passer des arguments à une fonction en fonction du nom du paramètre, plutôt que de la position du paramètre.

Nous pouvons passer des arguments nommés à une fonction en ajoutant simplement le nom du paramètre avant sa valeur :

callFunction(name: $value);

Nous sommes également autorisés à utiliser des mots-clés réservés, comme le montre l’exemple ci-dessous :

callFunction(array: $value);

Mais nous ne sommes pas autorisés à passer un nom de paramètre de manière dynamique. Le paramètre doit être un identifiant et la syntaxe suivante n’est pas autorisée :

callFunction($name: $value);

Selon Nikita Popov, l’auteur de ce RFC, les arguments nommés offrent plusieurs avantages.

Tout d’abord, les arguments nommés nous aideront à écrire un code plus compréhensible parce que leur signification est autodocumentée. L’exemple ci-dessous, tiré du RFC, s’explique d’lui-même :

array_fill(start_index: 0, num: 100, value: 50);

Les arguments nommés sont indépendants de l’ordre. Cela signifie que nous ne sommes pas obligés de transmettre les arguments à une fonction dans le même ordre que la signature de la fonction :

array_fill(value: 50, num: 100, start_index: 0);

Il est également possible de combiner des arguments nommés avec des arguments de position :

htmlspecialchars($string, double_encode: false);

Un autre grand avantage des arguments nommés est qu’ils permettent de spécifier uniquement les arguments que nous voulons réellement changer et que nous n’avons pas à spécifier d’arguments par défaut si nous ne voulons pas écraser les valeurs par défaut. L’exemple suivant, tiré de la RFC, le montre clairement :

htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode: false);

.

Named arguments can be used with PHP attributes, as shown in the following example from the RFC:

Les arguments nommés peuvent être utilisés avec les attributs PHP, comme le montre l’exemple suivant de la RFC :

<<MyAttribute('A', b: 'B')>>
class Test {}

Toutefois, il n’est pas permis de faire passer des arguments de position après des arguments nommés, ce qui entraînerait une erreur de compilation. La même chose se produit lorsque l’on passe deux fois le même nom de paramètre.

Les arguments nommés sont particulièrement utiles pour les déclarations de classe car les constructeurs ont généralement un grand nombre de paramètres et les arguments nommés fournissent une manière plus « ergonomique » de déclarer une classe.

Pour en savoir plus sur les « Named Arguments », avec les contraintes, les incompatibilités rétroactives et plusieurs exemples, voir le RFC « Named Arguments ».

Opérateur Nullsafe

Ce RFC introduit l’opérateur nullsafe $-> avec évaluation complète du court-circuit.

Dans l’évaluation de court-circuit, le second opérateur n’est évalué que si le premier opérateur n’évalue pas à null. Si un opérateur d’une chaîne est évalué à null, l’exécution de la chaîne entière s’arrête et est évaluée à null.

Examinez les exemples suivants du RFC :

$foo = $a?->b();

Si $a est nul, la méthode b() n’est pas appelée et $foo est mis à null.

Voir l’opérateur nullsafe RFC pour des exemples supplémentaires, les exceptions et le champ d’application futur.

Comparaisons entre les chaînes de Saner et les numéros

Dans les versions précédentes de PHP, lors d’une comparaison non stricte entre des chaînes de caractères et des nombres, PHP attribue d’abord la chaîne à un nombre, puis effectue la comparaison entre des nombres entiers ou flottants. Même si ce comportement est très utile dans plusieurs scénarios, il peut produire des résultats erronés qui peuvent également conduire à des bogues et/ou des problèmes de sécurité.

Considérons l’exemple suivant du RFC :

$validValues = ["foo", "bar", "baz"];
$value = 0;
var_dump(in_array($value, $validValues));
// bool(true)

Le PHP 8 introduit les comparaisons entre chaînes de Saner et nombres, visant à rendre les comparaisons entre chaînes et nombres plus raisonnables. Selon les termes de Nikita Popov,

Ce RFC vise à donner aux comparaisons de chaînes de chiffres un comportement plus raisonnable : Lors de la comparaison avec une chaîne numérique, utilisez une comparaison de nombres (comme maintenant). Sinon, convertissez le nombre en chaîne de caractères et utilisez une comparaison de chaînes de caractères.

Le tableau suivant compare le comportement de la comparaison des chaînes et des numéros dans les versions antérieures de PHP et dans PHP 8 :

Comparison    | Before | After
------------------------------
 0 == "0"     | true   | true
 0 == "0.0"   | true   | true
 0 == "foo"   | true   | false
 0 == ""      | true   | false
42 == "   42" | true   | true
42 == "42foo" | true   | false

Pour en savoir plus sur les nombreuses implications de ce changement et sur la façon dont les comparaisons entre chaînes et nombres changent en PHP 8, lisez le RFC officiel de Nikita Popov.

Saner Chaînes numériques

En PHP, les chaînes de caractères contenant des chiffres se répartissent en trois catégories :

  • Chaînes numériques : chaînes contenant un nombre éventuellement précédé d’espaces.
  • Chaîne numérique de tête : chaînes dont les caractères initiaux sont des chaînes numériques et les caractères finaux des chaînes non numériques.
  • Chaîne non numérique : chaînes n’entrant dans aucune des catégories précédentes.

Les chaînes numériques et les chaînes numériques principales sont traitées différemment selon l’opération effectuée. Par exemple, les chaînes numériques et les chaînes de chiffres de tête sont traitées différemment selon l’opération effectuée :

  • Les conversions explicites de chaînes de caractères en nombres (c’est-à-dire les caractères (int) et (float)) convertissent les nombres numériques et les chaînes de caractères à chiffres de tête. La conversion explicite d’une chaîne non numérique en un nombre produit 0.
  • Les conversions implicites de chaînes de caractères en nombres (c’est-à-dire sans déclaration de strict_type) donnent des résultats différents pour les chaînes de caractères numériques et non numériques. Les conversions de chaînes non numériques en nombres entraînent une erreur de type (TypeError).
  • is_numeric() renvoie vrai uniquement pour les chaînes de caractères numériques.

Les décalages de chaîne, les opérations arithmétiques, les opérations d’incrémentation et de décrémentation, les comparaisons de chaîne à chaîne et les opérations bit par bit donnent également des résultats différents.

Ce RFC se propose de :

Unifier les différents modes de chaînes numériques en un seul concept : Caractères numériques uniquement avec les espaces avant et arrière autorisés. Tout autre type de chaîne n’est pas numérique et entraînera des erreurs typographiques lorsqu’il sera utilisé dans un contexte numérique.

Cela signifie que toutes les chaînes qui émettent actuellement le E_NOTICE « A non well formed numeric value encountered » seront reclassées en E_WARNING « A non-numeric value encountered » sauf si la chaîne numérique de tête ne contenait que des espaces à la fin. Et les différents cas qui émettent actuellement un E_WARNING seront promus en TypeErrors.

Pour un aperçu plus détaillé des chaînes numériques en PHP 8, avec des exemples de code, des exceptions et des problèmes de rétrocompatibilité, voir le RFC.

Expression de correspondance v2

La nouvelle expression de correspondance est match similaire à switch, mais avec une sémantique plus sûre et permettant de renvoyer des valeurs.

To understand the difference between the two control structures, consider the following switch example from the RFC:

Pour comprendre la différence entre les deux structures de contrôle, examinez l’exemple de switch suivant du RFC :

switch (1) {
	case 0:
		$result = 'Foo';
		break;
	case 1:
		$result = 'Bar';
		break;
	case 2:
		$result = 'Baz';
		break;
}
 
echo $result;
//> Bar

Nous pouvons maintenant obtenir le même résultat que le code ci-dessus avec l’expression de match suivante :

echo match (1) {
	0 => 'Foo',
	1 => 'Bar',
	2 => 'Baz',
};
//> Bar

Un grand avantage de l’utilisation de la nouvelle expression match est que, alors que switch compare les valeurs de façon lâche (==), ce qui peut conduire à des résultats inattendus, avec match, la comparaison est un contrôle d’identité (===).

L’expression de match peut également contenir plusieurs expressions séparées par des virgules, ce qui permet une syntaxe plus concise (source) :

$result = match ($x) {
	// This match arm:
	$a, $b, $c => 5,
	// Is equivalent to these three match arms:
	$a => 5,
	$b => 5,
	$c => 5,
};

Pour d’autres exemples et cas d’utilisation, voir le RFC Match expression v2 et la documentation PHP.

Contrôles de type plus stricts pour les opérateurs arithmétiques/bit-système

Dans les versions précédentes de PHP, l’application d’opérateurs arithmétiques et bitwise à un tableau, une ressource ou un objet non surchargé était autorisée. Quoi qu’il en soit, le comportement était parfois incohérent.

Dans ce RFC, Nikita Popov montre à quel point ce comportement peut être déraisonnable à l’aide d’un simple exemple :

var_dump([] % [42]);
// int(0)

Nikita explique comment l’application d’un opérateur arithmétique ou binaire à des tableaux, des ressources ou des objets non surchargés a conduit à des résultats différents :

Opérateurs +, -, *, /, ** :

  • Lancez l’exception d’erreur sur l’opérande de tableau. (Excluant + si les deux opérandes sont des tableaux).
  • Convertir silencieusement un opérande de ressource en ID de ressource sous forme d’entier.
  • Convertir un opérande objet en entier un, tout en lançant un avis.

Opérateurs %, <<, >>, &, |, ^ :

  • Convertir silencieusement un opérande de tableau en entier zéro s’il est vide ou en entier un s’il n’est pas vide.
  • Convertir silencieusement un opérande de ressource en ID de ressource sous forme d’entier.
  • Convertir un opérande objet en entier un, tout en lançant un avis.

Opérateur ~ :

  • Lancez une exception d’erreur pour les opérandes de tableau, de ressource et d’objet.

Opérateurs ++ et — :

  • Ne faites rien en silence si l’opérande est un tableau, une ressource ou un objet.

Avec PHP 8, les choses changent et le comportement est le même pour tous les opérateurs arithmétiques et binaires :

Lancez une exception TypeError pour les opérandes de tableau, de ressource et d’objet.

Nouvelles fonctions PHP

PHP 8 apporte plusieurs nouvelles fonctions au langage :

str_contains

Avant PHP 8, strstr et strpos étaient les options typiques pour les développeurs qui cherchaient un needle dans une chaîne donnée. Le problème est que ces deux fonctions ne sont pas considérées comme très intuitives et que leur utilisation peut être déroutante pour les nouveaux développeurs PHP. Voir l’exemple suivant :

$mystring = 'Managed WordPress Hosting';
$findme = 'WordPress';
$pos = strpos($mystring, $findme);

if ($pos !== false) {
	echo "The string has been found";
} else {
	echo "String not found";
}

Dans l’exemple ci-dessus, nous avons utilisé l’opérateur de comparaison !==, qui vérifie également si deux valeurs sont du même type. Cela nous évite d’obtenir une erreur si la position de needle est 0 :

« Cette fonction peut renvoyer le booléen FALSE, mais peut également renvoyer une valeur non booléenne qui évalue à FALSE. […] Utilisez l’opérateur === pour tester la valeur de retour de cette fonction. »

En outre, plusieurs frameworks fournissent des fonctions d’aide pour rechercher une valeur à l’intérieur d’une chaîne donnée (voir la documentation de Laravel Helpers à titre d’exemple).

Maintenant, cette RFC propose l’introduction d’une nouvelle fonction permettant de rechercher à l’intérieur d’une chaîne de caractères : str_contains.

str_contains ( string $haystack , string $needle ) : bool

Son utilisation est assez simple. str_contains vérifie si $needle est trouvé dans $haystack et renvoie true ou false en conséquence.

Ainsi, grâce à str_contains, nous pouvons écrire le code suivant :

$mystring = 'Managed WordPress Hosting';
$findme   = 'WordPress';

if (str_contains($mystring, $findme)) {
	echo "The string has been found";
} else {
	echo "String not found";
}

Ce qui est plus lisible et moins sujet à des erreurs (voir ce code en action ici).

Au moment de la rédaction du présent document, str_contains est sensible à la casse, mais cela pourrait changer à l’avenir.

La proposition str_contains a été adoptée par 43 voix contre 9.

str_starts_with() et str_ends_with()

En plus de la fonction str_contains, deux nouvelles fonctions permettent de rechercher un needle à l’intérieur d’une chaîne donnée : str_starts_with et str_ends_with.

Ces nouvelles fonctions vérifient si une chaîne donnée commence ou se termine par une autre chaîne :

str_starts_with (string $haystack , string $needle) : bool
str_ends_with (string $haystack , string $needle) : bool

Les deux fonctions renvoient false si $needle est plus long que $haystack.

Selon Will Hudgins, l’auteur de cette RFC,

« Les fonctionnalités str_starts_with et str_ends_with sont tellement nécessaires que de nombreux frameworks PHP majeurs les prennent en charge, notamment Symfony, Laravel, Yii, FuelPHP et Phalcon« .

Grâce à eux, nous avons pu éviter d’utiliser des fonctions sous-optimales et moins intuitives comme substr, strpos. Ces deux fonctions sont sensibles à la casse :

$str = "WordPress";
if (str_starts_with($str, "Word")) echo "Found!";

if (str_starts_with($str, "word")) echo "Not found!";

Vous pouvez voir ce code en action ici.

Cette RFC a été approuvé par 51 voix contre 4.

get_debug_type

get_debug_type est une nouvelle fonction PHP qui renvoie le type d’une variable. La nouvelle fonction fonctionne de manière assez similaire à la fonction gettype, mais get_debug_type retourne les noms de type natifs et résout les noms de classe.

C’est une bonne amélioration pour le language, car gettype() n’est pas utile pour la vérification de type.

La RFC fournit deux exemples utiles pour mieux comprendre la différence entre la nouvelle fonction get_debug_type() et gettype(). Le premier exemple montre gettype en action :

$bar = [1,2,3];

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' . Foo::class . ', got ' . (is_object($bar) ? get_class($bar) : gettype($bar)));
}

Avec PHP 8, nous pourrions utiliser get_debug_type à la place :

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' . Foo::class . ' got ' . get_debug_type($bar));
}

Le tableau suivant affiche les valeurs de retour de get_debug_type et gettype :

Valeur gettype() get_debug_type()
1 integer int
0.1 double float
true boolean bool
false boolean bool
null NULL null
« WordPress » string string
[1,2,3] array array
Une classe qui s’appelle « Foo\Bar » object Foo\Bar
Une classe anonyme object class@anonymous

RFCs supplémentaires

Voici une liste succincte des améliorations supplémentaires approuvées pour le PHP 8 :

  1. Interface Stringable : cette RFC introduit une Interface Stringable qui est automatiquement ajoutée aux classes implémentant la méthode __to String(). Le but principal est ici d’utiliser le type d’union string|Stringable.
  2. Nouvelles API DOM Living Standard en ext/dom : cette RFC propose d’implémenter l’actuelle DOM Living Standard à l’extension PHP DOM en introduisant de nouvelles interfaces et propriétés publiques.
  3. Type de retour statique : PHP 8 introduit l’utilisation de static comme type de retour à côté des types self et parent.
  4. Ajustements de la syntaxe des variables : cette RFC résout certaines incohérences résiduelles dans la syntaxe des variables de PHP.

Benchmarks des performances de PHP 8

Si vous vous demandez quelle est la vitesse de PHP 8, nous avons la réponse. Nous avons comparé 20 plateformes/configurations PHP sur 7 versions différentes de PHP (5.6, 7.0, 7.1, 7.2, 7.3 et 8.0).

PHP 8.0 est sorti vainqueur dans la plupart des plateformes qui le prennent en charge, y compris WordPress et Laravel.

Compilation des benchmarks PHP des meilleures plateformes
Compilation des benchmarks PHP des meilleures plateformes

Par exemple, WordPress sur PHP 8.0 peut gérer 18,4 % de requêtes supplémentaires par seconde par rapport à PHP 7.4. De même, Laravel sur PHP 8.0 peut utiliser 8,5 % de requêtes par seconde en plus que PHP 7.3.

Si votre site web ou votre application est entièrement compatible avec PHP 8.0, vous devriez planifier la mise à jour de l’environnement de votre serveur vers PHP 8.0 dès que possible. Vous (et vos utilisateurs) apprécierez certainement ses avantages en termes de performances. Cependant, veuillez tester votre site de manière approfondie avant de procéder à la mise à jour.

Vous pouvez lire notre article sur les benchmarks PHP pour plus d’informations, comme des données détaillées sur les performances, des aperçus et de jolis graphiques !

Résumé

Quelle aventure ! Dans ce billet, nous avons couvert les optimisations et les fonctionnalités les plus intéressantes de PHP 8. La plus attendue est sûrement le compilateur Just in Time, mais il y a tellement plus avec PHP 8.

N’oubliez pas d’ajouter ce billet de blog à vos favoris pour votre future référence. 🤓

C’est maintenant votre tour : êtes-vous prêt à tester les nouvelles fonctionnalités de PHP ? Laquelle est votre préférée ? Écrivez-nous dans la section « Commentaires » ci-dessous.

Carlo Daniele Kinsta

Carlo is a passionate lover of webdesign and front-end development. He has been playing with WordPress for more than 20 years, also in collaboration with Italian and European universities and educational institutions. He has written hundreds of articles and guides about WordPress, published both on Italian and international websites, as well as on printed magazines. You can find him on LinkedIn.