Les nouveautés de PHP8

Version alpha-1 prévue le 25 juin 2020 !

Alexandre Tranchant

Comme chaque nouvelle version majeure d’un outil de développement, la huitième version de PHP est attendue avec impatience par beaucoup de développeurs. Elle viendra avec son lot de nouveautés que nous pourrons tester en version alpha 1 dès le 25 juin !

Il nous est annoncé des améliorations de performance, mais aussi beaucoup de nouveautés. C’est ce que nous allons détailler dans cet article.

Commentez

- Titre : Les nouveautés de PHP8

- Auteur : Alexandre Tranchant

- Parution : 20 juin 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Alexandre Tranchant. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Au programme…

- II. Les nouvelles fonctionnalités

- II-A. « Union types » : Les types à choix multiples

- II-B. La compilation à la volée : JIT

- II-C. La simplification du code des constructeurs

- II-D. Les attributs

- II-E. Les WeakMap

- II-F. Le type « static » pour les résultats

- II-G. Le type mixed

- II-H. Les exceptions

- II-H-1. Le mot-clé throw devient une expression

- II-H-2. Fin de l’obligation d’affecter une variable à l’exception levée

- II-I. Les chaînes de caractères

- II-I-1. La nouvelle interface Stringable

- II-I-2. La fonction str_contains

- II-I-3. Les fonctions str_start_with et str_end_with

- II-J. Les autres nouvelles fonctions

- II-J-1. get_debug_type

- II-J-2. fdiv

- II-J-3. get_resource_id

- II-K. Les autres nouveautés, en vrac…

- III. Les changements à prendre en compte pour migrer vers PHP 8

- III-A. Les index négatifs et les tableaux

- III-B. Amélioration du polymorphisme

- III-C. Les changements dans la gestion des erreurs

- III-C-1. La configuration des remontées d’erreur

- III-C-2. Changement de niveau pour certaines erreurs

- III-C-3. Les opérandes incorrects

- III-D. La conversion de nombres flottants en chaînes de caractères

- III-E. Les autres changements

- IV. Et vous qu’en pensez-vous ?

- V. Sources

I - Au programme…

Voici l’ambitieux programme de PHP 8 :

- de nouvelles fonctionnalités :

- un compilateur JIT,

- les « union types » que je traduirais bien par des types à choix multiples,

- les attributs,

- une nouvelle classe WeakMap,

- les simplifications de code apportées par les nouveaux constructeurs,

- de nouvelles fonctions ;

- des changements qui auront un impact sur le code existant (Breaking changes) :

- la gestion de l’incrémentation d’index négatifs dans les tableaux,

- des modifications sur la gestion des erreurs,

- des modifications sur les valeurs par défaut des directives d’initialisation de PHP,

- les fonctions supprimées.

Cet article est conçu de la façon suivante : chaque nouveauté s’accompagne d’un lien vers la RFC officielle pour que vous puissiez approfondir le sujet. Chaque fois que c’est possible, un exemple de code sera fourni. Vous voudrez bien pardonner notre outil de coloration syntaxique qui pourrait perdre son latin, ou du moins son PHP, avec tant de nouveautés.

II - Les nouvelles fonctionnalités

II-A - « Union types » : Les types à choix multiples

- Date de proposition : septembre 2019.

- Auteur de la proposition : Nikita Potov.

- Statut : implémenté !

- RFC : https://wiki.php.net/rfc/union_types_v2.

- Résultat des votes : 61 votes POUR et 5 votes CONTRE. N’hésitez pas à donner votre point de vue sur le forum !

Depuis la version 7.4 de PHP, vous pouvez préciser le type de toute propriété d’une classe. Si vous ne le précisez pas, comme en version 7.3 et précédentes, alors cette propriété accepte n’importe quel type, avec tous les risques que cela peut entraîner. Si le type est précisé et que vous ne le respectez pas, PHP provoque une erreur. Cette RFC « Union type » permet à une propriété de ne plus accepter un seul type défini, mais plusieurs.

Admettons que vous vouliez que la propriété de votre classe soit un entier ou nombre flottant, auparavant, vous n’aviez d’autre choix que de le noter en commentaire et d’espérer que les développeurs respectent vos contraintes. Votre objet ressemblait à cela :

Déclaration d'une propriété de type int ou float en PHP7

class Nombre {
/**
* @var int|float $nombre
*/
private $nombre;

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

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

Les plus exigeants pouvaient aussi ajouter un validateur dans le setter et générer des exceptions. Désormais, en PHP 8, ce sera beaucoup plus simple. Vous pourrez déclarer un ensemble de types en les séparant par une barre verticale, le caractère « pipe » : | .

La même classe en PHP 8

class Nombre {
private int|float $nombre;

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

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

Pas mal ! J’y vois un intérêt immédiat pour les types décimaux de Doctrine. Ce type décimal est un nombre décimal quand il est stocké en base de données avec un nombre précis de chiffres avant et un autre nombre précis de chiffres après la virgule. Ce type n’existant pas en PHP, pour éviter tout problème d’arrondi avec les nombres réels, Doctrine convertit ce nombre flottant en chaîne de caractères, avant de le renvoyer à votre modèle de données. Le développeur gère donc tantôt une chaîne, tantôt un nombre flottant. Les types à choix multiples sont une excellente solution pour ce cas précis.

II-B - La compilation à la volée : JIT

- Date de proposition : janvier 2019.

- Auteurs : Dmitry Stogov et Zeev Suraski.

- Statut : accepté et implémenté dans certaines branches dédiées.

- RFC : https://wiki.php.net/rfc/jit.

- Votes : 50 votes POUR ; 2 votes CONTRE.

JIT est l’acronyme de « Just In Time » : compilation à la volée en français. Cette optimisation a été plusieurs fois repoussée :

- les gains de performance n’étaient pas assez importants ;

- le code devenait trop complexe à développer et à maintenir ;

- il existait d’autres optimisations possibles avant celle-ci. (OPCache, par exemple.)

D’ailleurs, si vous regardez le détail des votes, le déploiement de JIT dans la version 7.4 avait été rejeté par les électeurs, quand bien même cette extension aurait été inscrite comme expérimentale : 18 votes POUR ; 36 votes CONTRE. Il faut dire que les enjeux sont importants.

À terme, l’administrateur du serveur pourra modifier dans le fichier de configuration un ensemble de paramètres pour activer ou non la compilation à la volée, changer la taille du cache, mais aussi certains déclencheurs. Je pensais au départ qu’il n’y aurait aucun impact dans le code, mais il semble que nous pourrons utiliser l’annotation « @jit », dans notre code pour cibler certaines fonctions. D’ailleurs, à mon sens, cela ne devrait pas être une annotation. Cela devrait respecter cette autre nouveauté de PHP 8 : les attributs.

Alors, le gain de performance ? Eh bien, ça semble très prometteur. Voici les fichiers de benchmark utilisés, un par langage. Le résultat de ce benchmark est passé de 46 ms en version 7.4 à 11 ms en version 8 ! Oui PHP 8, dans le cadre de ce test, est quatre fois plus rapide. Je précise que c’est bien : dans le cadre de ce test. Certes, les dés ne sont pas truqués comme pour les essais des moteurs diesel du constructeur Volkswagen, nous ne savons pas ce qu’il en sera pour la moyenne de tous les processus, mais c’est très prometteur !

D’autres tests ont été effectués, les plus mauvais parlent d’un gain de 5 %, d’autres d’un gain de 30 %, d’autres révèlent qu’on passe d’une moyenne de 320 ms à 140 ms. Les mauvaises langues diront que le gain est presque nul dans certains cas. D’autres comprendront qu’aucune perte n’a été mesurée, alors que cela arrive souvent : toucher un compilateur peut améliorer certains calculs au détriment d’autres. C’est arrivé pour certaines versions d’autres compilateurs. Je ne les cite pas, car je ne suis pas dans un esprit de comparaison, d’autant que le gain global était très satisfaisant. Après tout, il est tout à fait acceptable de perdre des points de performances sur certains calculs quand d’autres gains sont attendus. D’ailleurs, c’est ce que beaucoup de développeurs font quand ils implémentent un système de cache. Ils savent perdre du temps à la création du cache, pour en gagner par la suite. Le plus important, à mes yeux, est que les développeurs de PHP gardent toujours à l’esprit les objectifs de PHP : rendre des pages web, le plus rapidement possible.

D’autres enjeux peuvent entrer en ligne de compte. Restons sur PHP pour éviter toute polémique. La version 5.0 était plus lente que la version 4.4. Une honte selon certains ! En réalité, la version 5.0 apportait beaucoup de fonctionnalités supplémentaires par rapport à la version 4.4. Cette perte n’avait pas été seulement acceptée, elle avait été anticipée. Les développeurs auraient pu ne pas déployer cette version et attendre la version 5.1 qui réglait la plupart des latences. Cela n’a pas été leur choix. À mon sens, ils ont eu bien raison, les nouveautés de la version 5.0 étaient attendues et les mises à jour de la version 4.4 étaient toujours assurées. Cela laissait donc le choix au chef de projet de privilégier ce qu’il fallait en fonction des desiderata de ses clients.

N’hésitez pas à faire vos propres tests grâce à cette version de PHP compilée avec JIT : https://windows.php.net/downloads/snaps/ostc/jit-dynasm/20190315/.

Attention, cette version ne contient que le compilateur JIT, vous ne pourrez pas l’utiliser pour tester les autres fonctionnalités de PHP8.

Voici le code pour effectuer le même test de Mabdelbrot : https://gist.github.com/dstogov/12323ad13d3240aee8f1.

Attention, le fichier de test de Mabdelbrot doit être modifié, car il a été écrit pour les versions précédentes de PHP. Dans ce code, le constructeur doit être renommé. Toute méthode ayant le même nom que la classe n’est plus considérée comme un constructeur en PHP8. Ci-dessous vous trouverez le code modifié pour réaliser ce test !

Mabdelbrot

1. <?php

2. define("BAILOUT",16);

3. define("MAX_ITERATIONS",1000);

5. class Mabdelbrot

6. {

7. function __construct()

8. {

9. $d1 = microtime(1);

10. for ($y = -39; $y < 39; $y++) {

11. for ($x = -39; $x < 39; $x++) {

13. if ($this->iterate($x/40.0,$y/40.0) == 0)

14. echo("*");

15. else

16. echo(" ");

18. }

19. echo("\n");

20. }

21. $d2 = microtime(1);

22. $diff = $d2 - $d1;

23. printf("\nPHP Elapsed %0.3f\n", $diff);

24. }

27. function iterate($x,$y)

28. {

29. $cr = $y-0.5;

30. $ci = $x;

31. $zr = 0.0;

32. $zi = 0.0;

33. $i = 0;

34. while (true) {

35. $i++;

36. $temp = $zr * $zi;

37. $zr2 = $zr * $zr;

38. $zi2 = $zi * $zi;

39. $zr = $zr2 - $zi2 + $cr;

40. $zi = $temp + $temp + $ci;

41. if ($zi2 + $zr2 > BAILOUT)

42. return $i;

43. if ($i > MAX_ITERATIONS)

44. return 0;

45. }

46. }

47. }

49. ob_start();

50. $m = new Mabdelbrot();

51. ob_end_flush();

Vous pouvez observer dans l’image suivante les résultats sur mon ordinateur de développement. Notez que j’ai remplacé tous les caractères par des espaces, pour vous permettre de comparer les résultats sur la même capture d’écran !

Le temps de traitement passe de 1318 ms à 915 ms en PHP 8.

II-C - La simplification du code des constructeurs

- Date : mars 2020.

- Auteur : Nikita Popov.

- Statut : accepté. Cela sera testable, quand le statut sera : implémenté.

- Votes : 46 votes POUR ; 10 votes CONTRE.

- RFC : https://wiki.php.net/rfc/constructor_promotion.

Les langages objet sont particulièrement verbeux. Par exemple, en PHP, pour écrire un simple objet respectant le patron de conception DTO, il faut écrire quelque chose de la sorte :

Exemple de DTO en PHP7

1. class MessageDTO

2. {

3. public string $titre;

5. public string $contenu;

7. public DateTimeImmutable $publication;

9. public function __construct(

10. string $titre,

11. string $contenu,

12. DateTimeImmutable $publication

13. ) {

14. $this->titre = $titre;

15. $this->contenu = $contenu;

16. $this->publication = $publication;

17. }

18. }

Désormais, en PHP 8, voici un code au résultat et aux propriétés exactement identiques, mais dont le code est grandement simplifié :

Le même DTO en PHP 8

1. class MessageDTO

2. {

3. public function __construct(

4. public string $titre,

5. public string $contenu,

6. public DateTimeImmutable $publication

7. ) {}

8. }

C’est plus clair, non ? En ajoutant le mot public devant un argument, PHP crée une propriété publique correspondante. Quelle bonne nouvelle ! Le code des classes va y gagner en lisibilité.

Code pour tester notre DTO

1. $message = new MessageDTO('titre','contenu du message', new DateTimeImmutable());

2. echo "\n".$message->titre;

3. echo "\n".$message->contenu;

4. echo "\n".$message->publication->format('d/m/Y H:i:s');

À l’écriture de cet article, je ne peux toujours pas tester ce code.

En attendant de tester, concentrons-nous sur le fonctionnement. En fait, en arrière-plan, PHP va transformer le code simplifié en un code plus verbeux. Le constructeur va donc promouvoir les paramètres déclarés comme publics en propriétés publiques.

Malheureusement, je n’ai pas l’impression que cela fonctionnera pour les paramètres déclarés comme privés ou protégés. J’avais espéré aussi que nous pourrions simplifier les classes qui respectent le patron de conception des entités. (Les propriétés sont privées ou protégées et un getter et un setter publics permettent respectivement de les récupérer et de les modifier.)

II-D - Les attributs

- Date : mars 2020.

- Auteurs : Benjamin Eberlei, Martin Schröder.

- Statut : accepté.

- RFC : http://wiki.php.net/rfc/attributes_v2.

- Implémentation : https://github.com/php/php-src/pull/5394.

- Votes : 51 votes POUR ; un vote CONTRE.

Si je parlais d’annotations au lieu d’attributs, cette nouveauté serait peut-être plus parlante pour vous. En fonction des langages, le même concept a plusieurs noms :

- annotations en Java, mais aussi en PHP (ce fameux @ dans les commentaires) ;

- attributs en C++ ou en Rust ;

- décorateurs en Python.

Pour rester fidèle au nom de cette RFC, je vais m’en tenir au terme attribut dans la suite de cet article. Plusieurs communautés de développeurs PHP ont repris les principes des attributs, un pseudolangage dans les commentaires préfixé par @. Les développeurs utilisant PHPDoc ou Doctrine utilisent les attributs. Certains sans le savoir, comme dirait M. Jourdain.

Bref, si les développeurs étaient quasiment unanimes pour le développement de cette RFC, ils étaient moins tranchés pour choisir le symbole de l’attribut :

- 41 ont voté pour les doubles marqueurs inférieurs et supérieurs : << Attribut >> ;

- 12 ont voté pour l’arobase : @Attribut.

Alors, dans les faits, qu’est-ce que cela donne ? Eh bien, j’ai repris un exemple de la RFC pour l’implémentation d’une entité Doctrine. J’ai scindé l’exemple en deux parties de code. Le premier code ne devrait pas vous surprendre, il s’agit de ce qui existait déjà. Le second présente sa future implémentation avec PHP 8. Au passage, remarquez à la ligne 2 : Doctrine nomme bien cela un attribut depuis longtemps !

Une entité Doctrine avec les attributs @

1. <?php

2. use Doctrine\ORM\Attributes as ORM;

3. use Symfony\Component\Validator\Constraints as Assert;

5. /** @ORM\Entity */

6. class User

7. {

8. /** @ORM\Id @ORM\Column(type="integer"*) @ORM\GeneratedValue */

9. private $id;

11. /**

12. * @ORM\Column(type="string", unique=true)

13. * @Assert\Email(message="The email '{{ value }}' is not a valid email.")

14. */

15. private $email;

17. /**

18. * @ORM\Column(type="integer")

19. * @Assert\Range(

20. *      min = 120,

21. *      max = 180,

22. *      minMessage = "You must be at least {{ limit }}cm tall to enter",

23. *      maxMessage = "You cannot be taller than {{ limit }}cm to enter"

24. * )

25. */

26. protected $height;

28. /**

29. * @ORM\ManyToMany(targetEntity="Phonenumber")

30. * @ORM\JoinTable(name="users_phonenumbers",

31. *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},

32. *      inverseJoinColumns={@ORM\JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}

33. *      )

34. */

35. private $phonenumbers;

36. }

La même entité avec les attributs << >>

1. <?php

2. use Doctrine\ORM\Attributes as ORM;

3. use Symfony\Component\Validator\Constraints as Assert;

5. <<ORM\Entity>>

6. class User

7. {

8. <<ORM\Id>><<ORM\Column("integer")>><<ORM\GeneratedValue>>

9. private $id;

11. <<ORM\Column("string", ORM\Column::UNIQUE)>>

12. <<Assert\Email(array("message" => "The email '{{ value }}' is not a valid email."))>>

13. private $email;

15. <<Assert\Range(["min" => 120, "max" => 180, "minMessage" => "You must be at least {{ limit }}cm tall to enter"])>>

16. <<ORM\Column(ORM\Column::T_INTEGER)>>

17. protected $height;

19. <<ORM\ManyToMany(Phonenumber::class)>>

20. <<ORM\JoinTable("users_phonenumbers")>>

21. <<ORM\JoinColumn("user_id", "id")>>

22. <<ORM\InverseJoinColumn("phonenumber_id", "id", JoinColumn::UNIQUE)>>

23. private $phonenumbers;

24. }

Ne faites pas trop attention à la coloration syntaxique. L’éditeur est perdu. Au début, je préférais éviter le changement. J’aimais bien ces arobases, mais je dois reconnaître que la solution retenue présente plusieurs avantages  :

- le format @Attribut n’a pas de délimiteur de fin d’attribut, quoique le point-virgule était une option ;

- les symboles [[ Attributs ]] entrent en conflit avec la syntaxe courte des tableaux.

La RFC présente d’ailleurs un débat intéressant s’appuyant sur des critiques argumentées. N’hésitez pas à utiliser ce sujet de notre forum pour donner votre préférence !

II-E - Les WeakMap

Les instances de cette classe WeakMap s’utilisent tels des tableaux dont l’index n’accepte rien d’autre qu’un objet.

Exemple d'utilisation d'une WeakMap

1. $map = new WeakMap;

2. $obj = new stdClass;

3. $map[$obj] = 42;

5. var_dump($map);

6. // object(WeakMap)#1 (1) {

7. //   [0]=>

8. //   array(2) {

9. //     ["key"]=>

10. //     object(stdClass)#2 (0) {

11. //     }

12. //     ["value"]=>

13. //     int(42)

14. //   }

15. // }

$map n’est pas un tableau, comme on pourrait le croire à la ligne 3. Il s’agit bien d’un objet instancié à la ligne 2.

Ce qui est intéressant avec cette classe, c’est que la destruction de l’objet $obj détruit aussi la valeur 42 qui y avait été associée à la troisième ligne. Cette une faiblesse volontaire de cette association, d’où le nom WeakMap.

1. unset($obj);

2. var_dump($map);

3. // object(WeakMap)#1 (0) {}

C’est à la fois un avantage et un inconvénient :

- un avantage, car la destruction de l’objet entraîne la suppression de la valeur associée :

- cela évite toute fuite de mémoire,

- chaque instance de la classe reste ainsi itérable ;

- un désavantage, car il n’y a donc pas de possibilité d’énumérer les clés. Les conséquences sont nombreuses :

- on ne peut pas sérialiser l’objet,

- seuls des objets peuvent servir de clé,

- ajouter un élément avec [] génère une erreur.

À la différence de WeakReference, toute instance de cette classe peut-être clonée.

Le fichier de test de cette classe est particulièrement parlant.

II-F - Le type « static » pour les résultats

En version 7, une méthode pouvait déjà retourner les types « self » et « static ». En revanche, si l’on pouvait préciser le type « self », on ne pouvait pas préciser un résultat de type « static ». Ça explosait. Ce sera désormais possible en PHP 8, comme le précise cette RFC.

Le résultat de type static

1. <?php

2. class A {

3. // Le mot-clé self en fin de déclaration de méthode NE génère PAS une erreur en PHP7.

4. public static function getSelf(): self

5. {

6. return new self();

7. }

9. // Le mot-clé static en fin de déclaration de méthode GÉNÈRE une erreur en PHP7 et n’en générera plus en PHP8.

10. public static function getStatic(): static

11. {

12. return new static();

13. }

14. }

16. //Pour ceux qui ignorent la différence entre self et static, voici un exemple :

17. class B extends A {}

19. echo get_class(B::getSelf());   // A

20. echo get_class(B::getStatic()); // B

21. echo get_class(A::getSelf());   // A

22. echo get_class(A::getStatic()); // A

Cela ne va pas révolutionner le monde, mais c’était un manque assez troublant.

II-G - Le type mixed

Restons dans les types. Actuellement, les types primaires possibles sont les suivants :

- array ;

- bool ;

- callable ;

- int ;

- float ;

- null ;

- object ;

- resource ;

- string.

Cette RFC décrit l’implémentation du nouveau type « mixed ». En version 8, ce code ne génèrera plus d’erreur :

Exemple d’utilisation du type mixed

1. <?php

2. class Message {

4. protected mixed $sujet;

6. public function getSujet(): mixed

7. {

8. return $this->sujet;

9. }

11. public function setSujet(mixed $sujet): self

12. {

13. $this->sujet = $sujet;

15. return $this;

16. }

17. }

Dans le cas d’un héritage de la classe Message, il est intéressant de savoir que :

- les types des paramètres sont contravariants ;

- les types des résultats sont covariants ;

- les types des propriétés sont invariants.

Voici quelques exemples avec la classe Sms qui hérite de la classe Message pour mettre en évidence les différences entre contravariance, covariance et invariance :

La contravariance des types de paramètres

1. <?php

2. class Message {

3. public function setSujet(string $sujet) {}

4. }

6. class Sms extends Message

7. {

8. // Les types des paramètres sont contravariants, je PEUX étendre le type. (string to mixed)

9. public function setSujet(mixed $value) {}

10. }

12. class Message {

13. public function setSujet(mixed $sujet) {}

14. }

16. class Sms extends Message

17. {

18. // Les types des paramètres sont contravariants, je NE peux PAS préciser le type (mixed to string)

19. public function setSujet(string $value) {} //Plantage

20. }

La covariance des types de retour

1. <?php

2. class Message {

4. public function setSujet(): mixed {}

5. }

7. class Sms extends Message

8. {

9. // Les types des résultats sont covariants, je PEUX préciser le type (mixed to string).

10. public function setSujet(): string {}

11. }

13. class Message {

15. public function setSujet(): string {}

16. }

18. class Sms extends Message

19. {

20. // Les types des résultats sont covariants, je NE peux PAS élargir le type (string to mixed)

21. public function setSujet(): mixed {} //Plantage

22. }

Les types de propriétés sont invariants

1. <?php

2. class Message

3. {

4. public mixed $titre;

5. public int $messages;

6. public $publication;

7. }

9. class Sms extends Message

10. {

11. // Les types des propriétés sont invariants, je NE peux PAS préciser passer de mixed à string

12. public string $titre; //Plantage

14. // Les types des propriétés sont invariants, je NE peux PAS supprimer le type de titre, même pas mixed.

15. public $titre; //Plantage

17. // Les types des propriétés sont invariables, je NE peux PAS étendre passer le type de int à mixed

18. public mixed $messages; //Plantage

20. // Les types des propriétés sont invariants, je NE peux PAS ajouter un type à publication, même pas mixed.

21. public mixed $publication; //Plantage

22. }

Attention, une autre RFC a un impact sur les autres cas que « mixed ». Elle est détaillée plus loin dans cet article, mais en résumé, toute incohérence générera une erreur, il n’y aura plus d’avertissements.

II-H - Les exceptions

II-H-1 - Le mot-clé throw devient une expression

Voici une évolution que j’attendais avec impatience. Pour résumer la RFC, le « throw » n’est plus une déclaration (statement in english), mais une expression. Pardonnez mon abus de langage, mais j’aime dire : « Signaler une exception revient à une simple expression. »

Voici les huit exemples de la RFC que j’ai traduits, ils présentent la simplification du code :

Signaler une exception revient à une simple expression

1. <?php

2. // Si la condition n'est pas respectée, alors je génère une exception

3. $condition || throw new Exception();

4. $condition or throw new Exception();

6. // Si la condition est respectée, alors je génère une exception

7. $condition && throw new Exception();

8. $condition and throw new Exception();

10. // Les fonctions fléchées n'acceptant que des expressions simples, le code suivant plantait. « Mais ça, c'était avant ! »

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

13. // Test simplifié : la valeur est non nulle, sinon je génère une exception.

14. $value = $nullableValue ?? throw new InvalidArgumentException();

16. // Test simplifié : la valeur est vraie (au sens non strict), sinon je génère une erreur

17. $value = $falsableValue ?: throw new InvalidArgumentException();

19. // Test simplifié : si le tableau n'est pas vide, je le réinitialise, sinon je génère une exception de type argument invalide.

20. $value = !empty($array)

21. ? reset($array)

22. : throw new InvalidArgumentException();

Il semble bien que les développeurs de PHP8 aient pris le parti d’apporter beaucoup de simplifications dans le code. N’hésitez pas à donner votre avis sur cette simplification.

II-H-2 - Fin de l’obligation d’affecter une variable à l’exception levée

Toujours dans le but d’éviter de coder l’inutile, grâce à cette RFC, il n’est plus obligatoire d’affecter une variable à l’exception que vous interceptez. En d’autres termes, vous pouvez supprimer ce « $exception » que votre éditeur de code ne voulait pas voir, car il détectait une variable non utilisée.

Ce code est désormais valide

1. <?php

2. //Avant PHP 8

3. try {

4. // Cette fonction génère une exception

5. fonctionGenerantUneException();

6. } catch (Exception $variableQuiNeSertPas) {

7. echo 'Un message d’erreur qui n’utilise pas l’exception';

8. }

11. // En PHP 8.0

12. try {

13. // Cette fonction génère une exception

14. fonctionGenerantUneException();

15. } catch (Exception) { //C’est plus clair, non ?

16. echo 'Un message d’erreur qui n’utilise pas l’exception';

17. }

Après, on peut toujours débattre de l’intérêt d’intercepter une exception et de ne pas se servir de son contenu. En attendant, cette évolution éclaircira le code.

II-I - Les chaînes de caractères

II-I-1 - La nouvelle interface Stringable

La nouvelle interface Stringable garantit au développeur que les classes dépendantes implémentent bien la méthode __toString(). Cette méthode garantit la conversion de l’objet en chaîne de caractères.

Cela va même plus loin, car si on place cette interface comme type de paramètre, la fonction ou la méthode acceptera les paramètres suivants :

- toute classe qui implémente la fonction __toString(), donc ;

- une chaîne de caractères ;

- un entier, car PHP sait convertir un entier en chaîne de caractères à la volée ;

- un booléen pour la même raison ;

- un nombre flottant, toujours pour la même raison.

Voici la RFC qui décrit cette nouvelle interface. Vous remarquerez que son auteur, Nicolas Grekas, un des plus grands contributeurs du projet Symfony y fait déjà référence dans le projet Symfony tant cette interface est utile. Elle est d’ailleurs implémentée dans le « polyfill » de PHP 8.

Cette interface a fusionné avec la branche principale de PHP et peut déjà être testée pour ceux qui veulent compiler PHP. Remarquez la quantité de discussions qu’elle a occasionnées !

II-I-2 - La fonction str_contains

PHP ne disposait pas de fonction pour vérifier qu’une chaîne de caractères en contenait bien une autre. Les développeurs utilisaient souvent la fonction strpos. D’ailleurs, certains oubliaient parfois de vérifier qu’elle ne retournait pas une valeur strictement fausse. Quoi qu’il en soit, cette RFC déclare la méthode str_contains :

Exemples avec la méthode str_contains

1. <?php

3. str_contains('abc', 'a'); // true

4. str_contains('abc', 'd'); // false

6. // les arguments peuvent contenir une chaîne vide.

7. str_contains('abc', '');  // true

8. str_contains('', '');     // true

9. str_contains('', 'abc');  // false

11. // ce qui présente un peu plus d’intérêt quand un argument est une variable dont on ignore la valeur

12. str_contains('abc', $variable);

13. str_contains($variable, 'a');

II-I-3 - Les fonctions str_start_with et str_end_with

Je m’attendais à ce que le vote de cette RFC introduisant deux nouvelles fonctions soit moins tranché. J’ai souvenir d’une ancienne RFC où elle avait été refusée, mais je n’en retrouve pas la trace. Peut-être me trompé-je ?. Bref, ces deux nouvelles fonctions se basent sur la fonction du chapitre précédent. Elles permettent de détecter respectivement :

- si une chaîne de caractères commence par la seconde chaîne passée en argument ;

- si une chaîne de caractères se termine par la seconde chaîne passée en argument.

Quelques exemples de ces deux fonctions

1. $str = "debutMilieuFin";

2. str_starts_with($str, "deb") //true

3. str_starts_with($str, "Deb") //false - sensibilité à la casse

4. str_ends_with($str, "Fin")   //true

5. str_ends_with($str, "fin")   //false - sensibilité à la casse

7. // avec des chaînes vides:

8. str_starts_with("a", "") //true

9. str_starts_with("", "")  //true

10. str_starts_with("", "a") //false

11. str_ends_with("a", "")   //true

12. str_ends_with("", "")    //true

13. str_ends_with("", "a")   //false

II-J - Les autres nouvelles fonctions

II-J-1 - get_debug_type

Cette nouvelle fonction me semble prometteuse. La fonction gettype présente un défaut majeur, elle manque de précision, elle ne retourne pas les types natifs de PHP, elle ne précise pas le nom de la classe d’un objet, seulement le fait que c’est un objet.

Comparaison des retours entre get_debug_type et gettype

Il y a fort à parier que PHPUnit recodera ses assertions statiques qui évaluent les types de données avec cette fonction. Ayant contribué à l’écriture de nombreux tests en PHP et en Symfony, j’y vois également un autre intérêt : cette fonction va aider les équipes de testeurs pour vérifier que les ressources ouvertes par les développeurs sont correctement refermées.

On remarquera que PHP gagne en cohérence. Les nouvelles fonctions respectent la nouvelle graphie : chaque mot est séparé par un tiret bas. J’aurais néanmoins profité de cette RFC pour créer un nouvel alias get_type de la fonction gettype et j’aurais demandé la dépréciation de gettype.

II-J-2 - fdiv

La nouvelle fonction fdiv se comportera comme la fonction fmod. Construite autour de la fonction intdiv, elle autorisera la division par zéro et retournera l’infini positif ou négatif ou NAN en fonction du dividende et du diviseur.

Exemples de calculs avec fdiv

1. var_dump(fdiv(10, 3));     //float(3.3333333333333)

2. var_dump(fdiv(10., 3.));   //float(3.3333333333333)

3. var_dump(fdiv(-10., 2.5)); //float(-4)

4. var_dump(fdiv(10., -2.5)); //float(-4)

6. var_dump(fdiv(10., 0.));   //float(INF)

7. var_dump(fdiv(10., -0.));  //float(-INF)

8. var_dump(fdiv(-10., 0.));  //float(-INF)

9. var_dump(fdiv(-10., -0.)); //float(INF)

11. var_dump(fdiv(INF, 0.));   //float(INF)

12. var_dump(fdiv(INF, -0.));  //float(-INF)

13. var_dump(fdiv(-INF, 0.));  //float(-INF)

14. var_dump(fdiv(-INF, -0.)); //float(INF)

16. var_dump(fdiv(0., 0.));    //float(NAN)

17. var_dump(fdiv(0., -0.));   //float(NAN)

18. var_dump(fdiv(-0., 0.));   //float(NAN)

19. var_dump(fdiv(-0., -0.));  //float(NAN)

21. var_dump(fdiv(INF, INF));  //float(NAN)

22. var_dump(fdiv(INF, -INF)); //float(NAN)

23. var_dump(fdiv(-INF, INF)); //float(NAN)

24. var_dump(fdiv(-INF, -INF));//float(NAN)

26. var_dump(fdiv(0., INF));   //float(0)

27. var_dump(fdiv(0., -INF));  //float(-0)

28. var_dump(fdiv(-0., INF));  //float(-0)

29. var_dump(fdiv(-0., -INF)); //float(0)

31. var_dump(fdiv(NAN, NAN));  //float(NAN)

32. var_dump(fdiv(INF, NAN));  //float(NAN)

33. var_dump(fdiv(0., NAN));   //float(NAN)

34. var_dump(fdiv(NAN, INF));  //float(NAN)

35. var_dump(fdiv(NAN, 0.));   //float(NAN)

Mathématiquement, j’ai un peu de mal avec les lignes 16 à 24. fdiv ne peut pas deviner le résultat, mais de là, dire que le résultat n’est pas un nombre, c’est faux au sens mathématique. J’aurais préféré lire quelque chose comme UNKNOWN ou UNDEFINED. Que les développeurs Matlab ou Python n’hésitent pas à nous donner leur point de vue éclairé !

II-J-3 - get_resource_id

J’aime beaucoup la phrase de Nikita Popov dans la Pull-Request de cette nouvelle fonction : « This is a more obvious and type-safe form of (int) $resource. » Pour récupérer l’identifiant d’une ressource, les développeurs devaient convertir la ressource en entier. Pour un nouveau développeur, je trouve que cela n’allait pas de soi. D’ailleurs cela pose son lot de problèmes d’interprétation quand la valeur retournée est zéro, mais au moins cette fonction aura le mérite d’être claire.

Nikita Popov a conservé le même fonctionnement de la conversion en entier. Une ressource refermée ne retournera pas zéro !

II-K - Les autres nouveautés, en vrac…

Cette RFC autorise une virgule à la fin des paramètres d’une fonction pour un mode de fonctionnement identique aux arguments d’un tableau ou d’une liste.

La présence de la dernière virgule ne fait plus exploser le code

1. class Uri {

2. private function __construct(

3. ?string $scheme,

4. ?string $query,

5. ?string $fragment, // Cette virgule faisait tout planter avant PHP 8

6. ) {

7. $array = [

8. $scheme,

9. $query,

10. $fragment, // Alors qu’ici c’était accepté !

11. ];

12. }

13. }

Une autre nouveauté, l’extension ext-json n’est plus une extension, son code est intégré au noyau de PHP. Les développeurs n’auront plus besoin de vérifier sa présence. Pour le coup, je me demande comment nous allons gérer cela avec composer, il va falloir que composer considère que la version 8 possède cette extension, sinon on va au-devant de quelques problèmes pour les bibliothèques qui seront utilisables dans des projets PHP 7 et PHP 8.

III - Les changements à prendre en compte pour migrer vers PHP 8

L’ensemble des impacts de rétrocompatibilités sont listés dans les sources du projet.

III-A - Les index négatifs et les tableaux

Si la première clé de votre tableau était un nombre négatif, la clé suivante était zéro. Faites le test en utilisant array_fill. Cette RFC change la donne, la clé suivante sera toujours n+1, même si n est négatif.

La RFC donne plusieurs exemples qui vont vous éclairer sur les changements et leurs impacts.

Les quatre exemples de la RFC

1. $a = array_fill(-2, 3, true);

2. $b = [-2 => true, true, true];

3. $c = ["string" => true, -2 => true, true, true];

4. unset($c["string"]);

5. $d[-2] = true;

6. $d[] = true;

7. $d[] = true;

Avant PHP 8, toutes ces variables a, b, c et d résultent en un tableau dont les clés d’index sont les suivantes :

- -2, on pouvait s’y attendre ;

- 0, cette fois c’est plus étonnant ;

- 1, là encore on pouvait s’y attendre.

En PHP 8, grâce à cette RFC, les clés s’incrémentent à partir de -2 :

- -2 ;

- -1, voilà qui est plus cohérent ;

- 0.

Cette modification était nécessaire. Néanmoins, son impact sera sans aucun doute l’une des plus pernicieuses. Si une fonction disparaît, il est facile de la rechercher dans son code et de la remplacer. Pour ce qui concerne des index négatifs qu’on incrémente, c’est plus complexe à rechercher dans le code source.

Si PHP va y gagner en cohérence, certains développeurs vont néanmoins avoir des surprises et vont devoir revoir leur code ! D’ailleurs, j’ai quelques correctifs qui vont disparaître dans certains projets, car nous avions justement des bogues à cause de cette incrémentation étrange de PHP. Toutefois, pour aider à repérer le souci, les développeurs de PHP ont mis en place une dépréciation, mais soyez vigilant, cette dépréciation ne sera présente que dans la version 7.4 !

III-B - Amélioration du polymorphisme

Le polymorphisme apporté par les traits est amélioré par cette RFC qui a d’ailleurs obtenu l’unanimité des auteurs. Elle valide les méthodes abstraites des traits selon les règles des types de paramètres qui doivent être identiques. (Ou contravariants dans le cas de mixed.)

Cet exemple devrait planter

1. trait T {

2. abstract public function test(int $x);

3. }

5. class C {

6. use T;

8. // La ligne ci-dessous devrait planter, mais ce n’était pas le cas en PHP7

9. public function test(string $x) {

10. var_dump($x);

11. }

12. }

Désormais, les types seront contrôlés. Par conséquent, on génère une incompatibilité rétroactive pour tous les codes qui utilisaient des traits, mais dont les méthodes avaient des signatures incorrectes. Cette rétro-incompatibilité est acceptable, car, en théorie, un développeur ne devrait pas laisser passer ce genre de bogues.

III-C - Les changements dans la gestion des erreurs

III-C-1 - La configuration des remontées d’erreur

Ce chapitre synthétise les changements dans les déclarations d’initialisation et du fichier php.ini :

- la variable $php_errormsg n’est plus disponible, quel que soit le paramétrage de track_errors. En fait, le paramétrage de track_errors a disparu des directives d’initialisation ;

- par défaut, les erreurs E_ALL étaient reportées à l’exception des erreurs E_NOTICE et E_DEPRECATED. Ce ne sera plus le cas. Toutes les erreurs seront remontées par défaut ;

- la directive display_startup_errors est maintenant activée par défaut ;

- l’opérateur @ ne cachera plus les erreurs fatales. Le noyau Kernel.php de Symfony devra revoir son appel du fichier d’environnement (et c’est tant mieux, je n’aime pas les @, sources de bogues). Les fonctions qui interceptent les erreurs devront également être revues, car le numéro d’erreur ne sera plus zéro.

Le mode par défaut des erreurs PDO était : silencieux. En d’autres termes, toute erreur SQL ne générait ni erreur ni avertissement. Les nouveaux développeurs PHP avaient de quoi s’interroger quand ils ne voyaient comme erreur que « call to fetch() on non-object ». Quand on n’a pas l’habitude de PHP, on ne commence pas à chercher une erreur SQL en lisant un tel message. Cette RFC change la valeur par défaut. Les développeurs de PHP ont voté pour que le mode d’erreur par défaut soit PDO::ERRMODE_EXCEPTION, un choix que je partage.

III-C-2 - Changement de niveau pour certaines erreurs

Dans le cas d’héritage, de traits ou d’implémentation d’interface, les méthodes incompatibles génèreront toujours des erreurs, afin d’harmoniser les pratiques. Cette RFC vous donnera plus d’informations.

D’ailleurs, cette RFC-ci augmente beaucoup de niveaux :

- par exemple, ces avertissements deviennent des erreurs :

- tenter d’affecter une valeur à une propriété sur un non-objet,

- tenter d’ajouter un élément à un tableau dont la clé PHP_INT_MAX est déjà allouée,

- tenter d’utiliser un type invalide (tableau, objet) comme clé d’un tableau,

- tenter d’écrire sur un index de tableau alors qu’il s’agit d’une valeur scalaire,

- tenter de parcourir une variable non traversable et qui n’est pas un tableau non plus ;

- quant à ces notifications, elles deviennent désormais des avertissements :

- tenter de lire une variable indéfinie,

- tenter de lire une propriété indéfinie,

- tenter de lire une propriété d’une variable qui n’est pas un objet,

- tenter d’accéder à un index de tableau sur une variable qui n’en est pas un (de tableau),

- tenter de convertir un tableau en chaîne de caractères,

- tenter d’utiliser une ressource comme clé d’index d’un tableau,

- tenter d’utiliser la valeur nulle, un booléen ou un nombre flottant comme index d’une chaîne de caractères,

- tenter de lire un index d’une chaîne de caractères dont le nombre n’est pas existant (out-of-bounds string offset),

- tenter d’assigner une chaîne vide à un indice de chaîne de caractères ;

- quand vous passez autre chose qu’une variable par référence, il n’y a plus de notifications, mais c’est une exception qui est générée ;

- déclarer un paramètre obligatoire après un paramètre optionnel est désormais déprécié (sauf dans le cas d’un objet qui peut être initialisé à nul).

Les paramètres optionnels et obligatoires

1. <?php

3. function test($a = [], $b) {} // Ceci est déprécié.

5. function test2(Form $a = null, $b) {} // Ceci reste autorisé.

III-C-3 - Les opérandes incorrects

Certaines manipulations d’opérandes ne généraient pas la moindre erreur et retournaient d’ailleurs des résultats particulièrement étonnants quand on les utilisait avec des signes arithmétiques.

Exemple de comportement étrange

1. <?php

3. var_dump([] % [42]); //Retourne int(0)

5. $c = new class{};

6. $c++; // Tout allait bien pour PHP !

8. $a = [42];

9. $a++; // Là encore, tout va bien, alors que cela ne devrait pas

Cette RFC définit comment gérer ces incohérences : en générant une exception de type TypeError quand un tableau, un objet ou une ressource sont utilisés comme opérandes. Bien sûr, l’addition de deux tableaux reste possible !

III-D - La conversion de nombres flottants en chaînes de caractères

Enfin ! Nous autres, Français et Allemands, n’allons plus rencontrer ce fichu bogue de reconversion ! Je m’explique.

La conversion d’un nombre flottant en chaîne de caractères sera désormais toujours indépendante de la locale comme proposé dans cette RFC. Pour nous, Français, l’impact sera substantiel. Il va surtout supprimer de nombreux problèmes de conversion et de reconversion.

Démonstration des problèmes de double conversion en locale française

1. <?php

3. //Alors qu'en anglais, tout marche bien !

4. setlocale(LC_ALL, "en_US");

5. $en = 42.245;

6. var_dump((string) $en);        // string(6) "42.245"

7. var_dump((float)(string) $en); // float(42.245) tout va bien

9. //En français, la double conversion fait perdre les chiffres après la virgule !

10. setlocale(LC_ALL, "fr_FR");

11. $fr = 42.245;

12. var_dump(       (string) $fr); // string(6) "42,245"

13. var_dump((float)(string) $fr); // float(42) ça fait mal !!! Je peux vous garantir que bon nombre d’applications perdent des centimes dans leur calcul de TVA dès lors qu’on touche à la locale.

15. //En PHP 8 tout restera indépendant de la locale !

16. var_dump((string) $fr);        // string(6) "42.245"

17. var_dump((float)(string) $fr); // float(42.245) c'est mieux non ?

En fait, pour moi, c’était un bogue. J’aurais aimé que ce soit corrigé dans toutes les versions précédentes, mais l’équipe de développeur considère que c’est un changement fonctionnel qui casse la rétrocompatibilité et que cela ne peut donc être fait que lors d’une version majeure.

III-E - Les autres changements

Voici une liste complémentaire. Cette liste n’est pas exhaustive.

- Comme nous l’avons vu dans le test de performance de Mabdelbrot, les méthodes avec le même nom que la classe ne sont plus traitées comme des constructeurs de classe. La méthode __construct doit être systématiquement utilisée, c’est la seule à être reconnue comme constructeur de classe.

- On ne peut plus appeler une méthode dynamique de la même façon qu’on le fait avec une méthode statique.

- Les conversions automatiques ne sont plus appelées.

- Accéder à des constantes non déclarées explicitement résulte en une exception de classe Error. Auparavant elles étaient interprétées comme une chaîne de caractères et généraient un avertissement.

- Le troisième argument de la fonction define ne peut plus être « true ». En conséquence, les noms des constantes sont toujours sensibles à la casse.

- Des fonctions sont supprimées :

- __autoload ;

- convert_cyr_string (sans dépréciation) ;

- create_function ;

- each ;

- ezmlm_hash (sans dépréciation) ;

- fgetss ;

- get_magic_quotes_gpc ;

- hebrevc (sans dépréciation) ;

- money_format ;

- restore_include_path (sans dépréciation).

- La fonction array_key_exists ne peut plus être utilisée avec des objets, vous devrez utiliser isset ou property_exists.

- Utiliser le mot-clé parent dans une classe qui n’en a pas résultera en une erreur fatale à la compilation.

- La concaténation de caractères n’est plus prioritaire sur les additions et les soustractions. C’était un vieux bug, sans doute lié à une confusion entre le point et le signe multiplié. Il est enfin corrigé. La RFC ajoute une dépréciation en PHP 7.4 dès qu’une expression sans parenthèses contiendra successivement des signes « . », « + », « - ».

IV - Et vous qu’en pensez-vous ?

Pour ma part, je trouve que ces nouvelles fonctionnalités s’inscrivent dans le bon sens. Je reste quelque peu frustré par les constructeurs promouvant, cela ouvre tellement la voie à la simplification du code que j’aurais aimé que cela aille plus loin. Mais n’oublions pas que fin juin, ce n’est que la version « Alpha 1 » qui sort. Peut-être y aura-t-il plus en novembre !

J’aurais aimé pouvoir déclarer des propriétés en lecture seulement, comme décrit dans cette RFC. J’avoue que les trop nombreuses lignes de code dans les entités m’agacent par leur pseudo-inutilité.

Mais ces remarques restent somme toute à la marge. Le compilateur JIT est prometteur. J’ai pris un peu de temps pour tester cette nouvelle version sur certains de mes projets. J’ai été confronté à bien des difficultés à cause de l’enfer des dépendances. C’est fou comme ce numéro de version « PHP8 » pose des soucis à composer. Mais après quelques tricheries (en composant avec PHP7 et en exécutant avec PHP8), j’ai pu m’en sortir. Les résultats sont dans la totalité des cas bénéfiques. Les temps d’exécution diminuent toujours, quel que soit l’état d’activation d’OpCache ! Certes ils ne sont pas toujours notables quand on est déjà à l’ordre de la dizaine de millisecondes, mais quand on voit le temps d’exécution se diviser par deux pour des chargements de données, on se dit que c’est un grand pas.

N’hésitez pas à faire part de vos commentaires dans ce sujet !

V - Sources

- RFC de PHP.

- Sources de PHP.

- Planning prévisionnel de PHP8.

- Les discussions internes sur PHP, ouvertes au public grâce au site externals.io.

Merci à ClaudeLELOUP pour son attentive relecture orthographique !