I. Au programme…▲
Voici l’ambitieux programme de PHP 8 :
-
de nouvelles fonctionnalités :
- un compilateur JITJust In Time,
- 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 :
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 » : | .
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 !
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 !
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
class MessageDTO
{
public
string $titre
;
public
string $contenu
;
public
DateTimeImmutable $publication
;
public
function
__construct
(
string $titre
,
string $contenu
,
DateTimeImmutable $publication
) {
$this
->
titre =
$titre
;
$this
->
contenu =
$contenu
;
$this
->
publication =
$publication
;
}
}
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é :
2.
3.
4.
5.
6.
7.
8.
class MessageDTO
{
public
function
__construct
(
public
string $titre
,
public
string $contenu
,
public
DateTimeImmutable $publication
) {}
}
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é.
À 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 !
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
<?php
use
Doctrine\ORM\Attributes as
ORM;
use
Symfony\Component\Validator\Constraints as
Assert;
/** @ORM\Entity */
class
User
{
/** @ORM\Id @ORM\Column(type="integer"*) @ORM\GeneratedValue */
private
$id
;
/**
* @ORM\Column(type="string", unique=
true
)
* @Assert\Email(message="The email '{{ value }}' is not a valid email.")
*/
private
$email
;
/**
* @ORM\Column(type="integer")
* @Assert\Range(
* min =
120
,
* max =
180
,
* minMessage = "You must be at least {{ limit }}cm tall to enter",
* maxMessage = "You cannot be taller than {{ limit }}cm to enter"
* )
*/
protected
$height
;
/**
* @ORM\ManyToMany(targetEntity="Phonenumber")
* @ORM\JoinTable(name="users_phonenumbers",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=
true
)}
* )
*/
private
$phonenumbers
;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
<?php
use
Doctrine\ORM\Attributes as
ORM;
use
Symfony\Component\Validator\Constraints as
Assert;
<<
ORM\Entity>>
class
User
{
<<
ORM\Id>><<
ORM\Column("integer"
)>><<
ORM\GeneratedValue>>
private
$id
;
<<
ORM\Column("string"
,
ORM\Column::
UNIQUE)>>
<<
Assert\Email(array
("message"
=>
"The email '{{ value }}' is not a valid email."
))>>
private
$email
;
<<
Assert\Range([
"min"
=>
120
,
"max"
=>
180
,
"minMessage"
=>
"You must be at least {{ limit }}cm tall to enter"
]
)>>
<<
ORM\Column(ORM\Column::
T_INTEGER)>>
protected
$height
;
<<
ORM\ManyToMany(Phonenumber::
class
)>>
<<
ORM\JoinTable("users_phonenumbers"
)>>
<<
ORM\JoinColumn("user_id"
,
"id"
)>>
<<
ORM\InverseJoinColumn("phonenumber_id"
,
"id"
,
JoinColumn::
UNIQUE)>>
private
$phonenumbers
;
}
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 RFCPHP RFC: Attributes v2 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.
$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.
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 WeakReferenceDocumentation de la classe 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 RFCPHP RFC: Static return type.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?php
class
A {
// Le mot-clé self en fin de déclaration de méthode NE génère PAS une erreur en PHP7.
public
static
function
getSelf():
self
{
return
new
self
();
}
// 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.
public
static
function
getStatic():
static
{
return
new
static
();
}
}
//Pour ceux qui ignorent la différence entre self et static, voici un exemple :
class
B extends
A {}
echo get_class(B::
getSelf());
// A
echo get_class(B::
getStatic());
// B
echo get_class(A::
getSelf());
// A
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 RFCPHP RFC: Mixed Type v2 décrit l’implémentation du nouveau type « mixed ». En version 8, ce code ne génèrera plus d’erreur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<?php
class
Message {
protected
mixed $sujet
;
public
function
getSujet():
mixed
{
return
$this
->
sujet;
}
public
function
setSujet(mixed $sujet
):
self
{
$this
->
sujet =
$sujet
;
return
$this
;
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
<?php
class
Message {
public
function
setSujet(string $sujet
) {}
}
class
Sms extends
Message
{
// Les types des paramètres sont contravariants, je PEUX étendre le type. (string to mixed)
public
function
setSujet(mixed $value
) {}
}
class
Message {
public
function
setSujet(mixed $sujet
) {}
}
class
Sms extends
Message
{
// Les types des paramètres sont contravariants, je NE peux PAS préciser le type (mixed to string)
public
function
setSujet(string $value
) {}
//Plantage
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?php
class
Message {
public
function
setSujet():
mixed {}
}
class
Sms extends
Message
{
// Les types des résultats sont covariants, je PEUX préciser le type (mixed to string).
public
function
setSujet():
string {}
}
class
Message {
public
function
setSujet():
string {}
}
class
Sms extends
Message
{
// Les types des résultats sont covariants, je NE peux PAS élargir le type (string to mixed)
public
function
setSujet():
mixed {}
//Plantage
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?php
class
Message
{
public
mixed $titre
;
public
int $messages
;
public
$publication
;
}
class
Sms extends
Message
{
// Les types des propriétés sont invariants, je NE peux PAS préciser passer de mixed à string
public
string $titre
;
//Plantage
// Les types des propriétés sont invariants, je NE peux PAS supprimer le type de titre, même pas mixed.
public
$titre
;
//Plantage
// Les types des propriétés sont invariables, je NE peux PAS étendre passer le type de int à mixed
public
mixed $messages
;
//Plantage
// Les types des propriétés sont invariants, je NE peux PAS ajouter un type à publication, même pas mixed.
public
mixed $publication
;
//Plantage
}
Attention, une autre RFCPHP RFC: Always generate fatal error for incompatible method 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 RFCPHP RFC: throw expression, 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<?php
// Si la condition n'est pas respectée, alors je génère une exception
$condition
||
throw
new
Exception
();
$condition
or
throw
new
Exception
();
// Si la condition est respectée, alors je génère une exception
$condition
&&
throw
new
Exception
();
$condition
and
throw
new
Exception
();
// Les fonctions fléchées n'acceptant que des expressions simples, le code suivant plantait. « Mais ça, c'était avant ! »
$callable
=
fn() =>
throw
new
Exception
();
// Test simplifié : la valeur est non nulle, sinon je génère une exception.
$value
=
$nullableValue
??
throw
new
InvalidArgumentException();
// Test simplifié : la valeur est vraie (au sens non strict), sinon je génère une erreur
$value
=
$falsableValue
?:
throw
new
InvalidArgumentException();
// Test simplifié : si le tableau n'est pas vide, je le réinitialise, sinon je génère une exception de type argument invalide.
$value
=
!
empty($array
)
?
reset($array
)
:
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 RFCPHP RFC: non-capturing catches, 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<?php
//Avant PHP 8
try
{
// Cette fonction génère une exception
fonctionGenerantUneException();
}
catch
(Exception
$variableQuiNeSertPas
) {
echo 'Un message d’erreur qui n’utilise pas l’exception'
;
}
// En PHP 8.0
try
{
// Cette fonction génère une exception
fonctionGenerantUneException();
}
catch
(Exception
) {
//C’est plus clair, non ?
echo 'Un message d’erreur qui n’utilise pas l’exception'
;
}
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 interfacePHP RFC: Add Stringable 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 SymfonyLe projet Symfony utilise déjà l'interface 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 RFCPHP RFC: str_contains déclare la méthode str_contains :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php
str_contains('abc'
,
'a'
);
// true
str_contains('abc'
,
'd'
);
// false
// les arguments peuvent contenir une chaîne vide.
str_contains('abc'
,
''
);
// true
str_contains(''
,
''
);
// true
str_contains(''
,
'abc'
);
// false
// ce qui présente un peu plus d’intérêt quand un argument est une variable dont on ignore la valeur
str_contains('abc'
,
$variable
);
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 RFCPHP RFC: Add str_starts_with() and str_ends_with() functions 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
$str
=
"
debutMilieuFin
"
;
str_starts_with($str
,
"
deb
"
) //true
str_starts_with($str
,
"
Deb
"
) //false - sensibilité à la casse
str_ends_with($str
,
"
Fin
"
) //true
str_ends_with($str
,
"
fin
"
) //false - sensibilité à la casse
// avec des chaînes vides:
str_starts_with("
a
"
,
""
) //true
str_starts_with(""
,
""
) //true
str_starts_with(""
,
"
a
"
) //false
str_ends_with("
a
"
,
""
) //true
str_ends_with(""
,
""
) //true
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.
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 NANNot A Number en fonction du dividende et du diviseur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
var_dump(fdiv(10
,
3
));
//float(3.3333333333333)
var_dump(fdiv(10.
,
3.
));
//float(3.3333333333333)
var_dump(fdiv(-
10.
,
2.5
));
//float(-4)
var_dump(fdiv(10.
,
-
2.5
));
//float(-4)
var_dump(fdiv(10.
,
0.
));
//float(INF)
var_dump(fdiv(10.
,
-
0.
));
//float(-INF)
var_dump(fdiv(-
10.
,
0.
));
//float(-INF)
var_dump(fdiv(-
10.
,
-
0.
));
//float(INF)
var_dump(fdiv(INF,
0.
));
//float(INF)
var_dump(fdiv(INF,
-
0.
));
//float(-INF)
var_dump(fdiv(-
INF,
0.
));
//float(-INF)
var_dump(fdiv(-
INF,
-
0.
));
//float(INF)
var_dump(fdiv(0.
,
0.
));
//float(NAN)
var_dump(fdiv(0.
,
-
0.
));
//float(NAN)
var_dump(fdiv(-
0.
,
0.
));
//float(NAN)
var_dump(fdiv(-
0.
,
-
0.
));
//float(NAN)
var_dump(fdiv(INF,
INF));
//float(NAN)
var_dump(fdiv(INF,
-
INF));
//float(NAN)
var_dump(fdiv(-
INF,
INF));
//float(NAN)
var_dump(fdiv(-
INF,
-
INF));
//float(NAN)
var_dump(fdiv(0.
,
INF));
//float(0)
var_dump(fdiv(0.
,
-
INF));
//float(-0)
var_dump(fdiv(-
0.
,
INF));
//float(-0)
var_dump(fdiv(-
0.
,
-
INF));
//float(0)
var_dump(fdiv(NAN,
NAN));
//float(NAN)
var_dump(fdiv(INF,
NAN));
//float(NAN)
var_dump(fdiv(0.
,
NAN));
//float(NAN)
var_dump(fdiv(NAN,
INF));
//float(NAN)
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 RFCPHP RFC: Allow trailing comma in parameter list 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class Uri {
private
function
__construct
(
?
string $scheme
,
?
string $query
,
?
string $fragment
,
// Cette virgule faisait tout planter avant PHP 8
) {
$array
=
[
$scheme
,
$query
,
$fragment
,
// Alors qu’ici c’était accepté !
];
}
}
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ésImpacts de rétrocompatibilités de PHP8 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.
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 RFCPHP RFC: Validation for abstract trait methods 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.)
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 RFCPHP RFC: Change Default PDO Error Mode 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 RFCPHP RFC: Always generate fatal error for incompatible method signatures vous donnera plus d’informations.
D’ailleurs, cette RFC-ciPHP RFC: Reclassifying engine warnings 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).
2.
3.
4.
5.
<?php
function
test($a
=
[],
$b
) {}
// Ceci est déprécié.
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.
2.
3.
4.
5.
6.
7.
8.
9.
<?php
var_dump([]
%
[
42
]
);
//Retourne int(0)
$c
=
new
class
{};
$c
++;
// Tout allait bien pour PHP !
$a
=
[
42
];
$a
++;
// Là encore, tout va bien, alors que cela ne devrait pas
Cette RFCPHP RFC: Stricter type checks for arithmetic/bitwise operators 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 RFCPHP RFC: Locale-independent float to string cast. Pour nous, Français, l’impact sera substantiel. Il va surtout supprimer de nombreux problèmes de conversion et de reconversion.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
<?php
//Alors qu'en anglais, tout marche bien !
setlocale(LC_ALL,
"en_US"
);
$en
=
42.245
;
var_dump((string)
$en
);
// string(6) "42.245"
var_dump((float)(string)
$en
);
// float(42.245) tout va bien
//En français, la double conversion fait perdre les chiffres après la virgule !
setlocale(LC_ALL,
"fr_FR"
);
$fr
=
42.245
;
var_dump( (string)
$fr
);
// string(6) "42,245"
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.
//En PHP 8 tout restera indépendant de la locale !
var_dump((string)
$fr
);
// string(6) "42.245"
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 RFCPHP RFC: Change the precedence of the concatenation operator 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 RFCreadonly_properties. 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 PHPToutes les RFC de PHP.
- Sources de PHPCode sources de PHP sur Github.
- Planning prévisionnel de PHP8.
- Les discussions internes sur PHP, ouvertes au public grâce au site externals.ioOpening PHP's #internals to the outside..
Merci à ClaudeLELOUP pour son attentive relecture orthographique !