De PHP 7 à PHP 8, retour sur cinq ans d’innovation

Alors que PHP 8.1 va sortir à la fin de l’année, et que la version 21.10 d’Ubuntu vient d’être mise en ligne en embarquant PHP 8, je me suis dit que ça pourrait être bien de revenir sur toutes les nouveautés qui sont apparues dans le langage PHP depuis que PHP 7 a été présenté en décembre 2015.

Mon but ne va pas être de lister toutes les fonctionnalités qui sont apparues dans PHP 7, 7.1, 7.2, 7.3, 7.4 et 8 ; il y en a beaucoup trop, certaines étant très pointues et pensées pour des usages très particuliers. Je vais me contenter de parler de celles qui me paraissent les plus intéressantes, celles que j’utilise réellement dans mes développements.

PHP 7 (décembre 2015)

Pour en savoir plus

PHP 7 était une amélioration majeure, qui apportait notamment une réécriture du moteur interne et de la gestion des tableaux, amenant une augmentation importante des performances.

Il faut savoir que le code qui sert à gérer les tableaux (et les tableaux associatifs, car en PHP c’est la même chose) est utilisé en interne par la machine virtuelle PHP pour stocker tous les symboles (fonctions, objets, variables, etc.) ; donc améliorer la performance des tableaux, ça n’est pas utile que lorsque votre code manipule des tableaux, ça améliore toute l’exécution de PHP.
Si le sujet vous intéresse, et pour simplifier la chose à l’extrême, disons que les éléments d’un tableau (au sens PHP) sont stockés dans un tableau (au sens C) via une fonction de hachage ; mais pour pouvoir garder l’ordre dans lequel les éléments sont ajoutés, il faut constituer une liste. Auparavant, cela se faisait avec une liste doublement chaînée, alors que désormais cela utilise un tableau (au sens C). Les listes chaînées ne consomment que la mémoire nécessaire, mais l’allocation mémoire nécessaire à chaque ajout d’élément prend du temps ; l’utilisation de tableaux est beaucoup plus rapide, mais consomme un peu plus de mémoire (car on alloue de la place en prévision des éléments futurs).

Utilisation des types scalaires dans les déclarations de paramètres

Cela fait longtemps qu’on peut déclarer les types des paramètres attendus par les fonctions en méthodes, sauf que cela se limitait aux objets (et aux dérivés comme self et parent), aux interfaces, aux tableaux (array) et aux fonctions (callable). En PHP 7, il est possible de déclarer qu’un paramètre est de type string, int, float, ou bool. Ça a l’air idiot, mais c’est difficile de s’en passer.

function toto(string $s) {
    // dans la fonction, on est sûr que $s est une chaîne
}
toto('abc'); // OK
toto(123); // OK sauf en mode strict
toto(new MonObjet()); // erreur 

Déclaration du type de retour

Dans les fonctions et méthodes, PHP 7 permet de définir le type de retour. Comme pour les paramètres, PHP fera par défaut les conversions de type lorsqu’elles sont possibles (de int vers string, par exemple), sauf si on utilise le mode strict (si la fonction retourne une valeur du mauvais type, cela génère une erreur).
Ça permet d’avoir du code beaucoup plus propre.

function toto() : int { ... }
$a = toto(); // on est sûr de récupérer un entier

L’opérateur NULL coalescent

J’étais dubitatif sur l’utilité de cet opérateur, mais je l’utilise maintenant hyper souvent. Il fait l’équivalent d’un opérateur ternaire avec un isset() dans la condition.

$a = isset($_POST['valeur']) ? $_POST['valeur'] : $default;
$a = $_POST['valeur'] ?? $default; // équivalent à la ligne précédente

PHP 7.1 (décembre 2016)

Pour en savoir plus

Type nullable

Aussi bien dans les définitions de paramètres que pour les types de retour, il devient possible de dire que la valeur peut être soit du type demandé, soit nulle, en mettant un point d’interrogation devant le type.
C’est devenu indispensable.

function toto(?int $i) { ... }
toto(3); // OK
toto(null); // OK

Pseudo-type void dans les retours de fonctions

Les fonctions qui ne doivent jamais rien retourner peuvent prendre le pseudo-type void.
Ce n’est pas un truc révolutionnaire, mais ça permet là encore de rendre le code plus propre.

Déconstruction symétrique des tableaux

Cette fonctionnalité n’est pas hyper-utile, mais c’est le genre de sucre syntaxique qu’on est content d’avoir quand on en a besoin. Ça rend le code plus lisible, c’est toujours bon à prendre.

list($alice, $bob, $camille) = $users;
[$alice, $bob, $camille] = $users; // équivalent à la ligne précédente

catch multiple

Cette fonctionnalité permet de regrouper dans une même instruction catch plusieurs types d’exceptions. Ça évite d’avoir à écrire plusieurs catch avec exactement le même code.
Là encore, c’est du sucre syntaxique. Mais quand on a pris l’habitude, ça devient indispensable.

try {
// ...
} catch (UneException | AutreException $e) {
// ...
}

PHP 7.2 (novembre 2017)

Pour en savoir plus

Ben… il y a pas mal de nouveautés qui sont apparues dans PHP 7.2, mais aucune je n’utilise vraiment au quotidien.

PHP 7.3 (décembre 2018)

Pour en savoir plus

Virgules de fin dans les appels de fonctions

Dans les appels de fonctions et de méthodes, il est maintenant possible de laisser traîner une virgule après le dernier paramètre. Comme pour les tableaux, ça permet d’ajouter des entrées supplémentaires avec des diff de commit plus propres.

toto(
$param1,
$param2,
);

PHP 7.4 (novembre 2019)

Pour en savoir plus

Typage des attributs de classes

Il est maintenant possible de spécifier le type des attributs dans les objets.
Une fois qu’on y a goûté, c’est comme pour le typage des paramètres et des retours de fonctions, on ne peut plus s’en passer.

class Toto {
private int $entier;
private string $chaine;
}

Opérateur d’assignation de fusion null

Derrière ce nom barbare se cache un opérateur très pratique, qui évite d’utiliser un opérateur ternaire ou un opérateur null coalescent (voir plus haut).

$a = isset($a) ? $a : $b;
$a = $a ?? $b; // équivalent à la ligne précédente
$a ??= $b; // équivalent aux deux lignes précédentes

Déballage de tableaux

Là, il s’agit de lister automatiquement tous les éléments d’un tableau.
C’est du sucre syntaxique qui n’est pas utile tous les jours, mais qui nous économise plusieurs lignes de code quand on en a besoin.

$t1 = [3, 4];
$t2 = [1, 2, ...$t1, 5, 6]; // équivalent à [1, 2, 3, 4, 5, 6]

Underscores dans les nombres

C’est le genre de petit truc qui facilite tellement la lecture du code qu’on se demande comment on faisait avant.

$couleurs = 16_777_216; // plus lisible que 1677216
$max32bits = 4_294_967_296; // plus lisible que 4294967296

Préchargement de code dans OPCache

Cette fonctionnalité permet d’ajouter dans la configuration PHP un script dont le contenu sera lu au lancement de l’interpréteur, et dont le code sera chargé dans le cache d’opcode.
Quand on maîtrise un serveur et qu’on veut accélérer le chargement d’une application web, c’est très efficace.

Méthodes magiques __serialize() et __unserialize()

J’ai toujours trouvé que l’utilisation des méthodes magiques __sleep() et __wakeup() était bancale. L’interface Sérializable apportait les méthodes serialize() et unserialize(), mais c’est plus logique d’ajouter des méthodes magiques supplémentaires plutôt que d’être obligé d’implémenter une interface.

PHP 8 (décembre 2020)

Pour en savoir plus

Paramètres nommés

Cette fonctionnalité sert lorsqu’on appelle une fonction ou une méthode, et permet de ne pas donner des valeurs explicites à tous les paramètres qui précèdent un paramètre dont on a besoin de donner une valeur.
C’est une fonctionnalité que j’attends depuis longtemps, qui était bien pratique en Python.

function toto(int $nbr=0, string $nom=null, int $age=null) { ... }
toto(nom: 'Alice');
toto(nom: 'Bob', nbr: 4);
toto(3, age: 100);

Union de types

Dans les définitions de paramètres et de retours de fonctions, il devient possible de lister les types autorisés.
Je ne vous dis pas à quel point c’est pratique.

function toto(int|string|null $intput) : int|float { ... }

Pseudo-type mixed

Dans les paramètres et le retour d’une fonction ou d’une méthode, il est possible d’utiliser le pseudo-type mixed, qui sert à indiquer explicitement que cela peut être n’importe quel type. Précisément, c’est un alias de l’union object|resource|array|string|int|float|bool|null.
Ça peut sembler idiot, mais ça permet de dire très clairement qu’un paramètre peut être de n’importe quel type. Ça évite de se poser la question “Le développeur a-t-il oublié de mettre le type, ou bien est-ce que ça accepte n’importe quoi ?”.
Avant ça, je mettais un commentaire /*mixed*/ à la place du type, pour être explicite. Mais c’est encore mieux que ce soit géré nativement par le langage.

Pseudo-type de retour false

Il est possible d’indiquer false comme type de retour d’une méthode, pour dire qu’elle est susceptible de retourner false (par exemple en cas d’erreur), mais qu’elle ne peut pas retourner true ; donc ce n’est pas un type de retour booléen. Le cas bien connu est celui de la fonction strpos(), qui peut retourner un nombre (éventuellement zéro) ou false (quand la chaîne recherchée n’a pas été trouvée).
À noter que ça ne peut pas être un type autonome (il faut forcément faire une union avec un autre type), et qu’il n’y a pas de pseudo-type true.

function toto() : int|false { ... }

Opérateur nullsafe ?->

Cet opérateur permet d’accéder aux attributs et méthodes d’un objet. Mais si l’élément à gauche de l’opérateur vaut null, l’attribut ou la méthode n’est pas accédée et l’ensemble retourne null.
C’est extrêmement pratique lorsqu’on veut chaîner des appels de méthodes d’un même objet, mais que ces méthodes sont susceptibles de retourner null. Habituellement, on est obligé d’ajouter plein de vérifications (et certaines personnes vous diront qu’il ne faut pas chaîner, c’est mal ; eh bien non seulement vous savez ce que je pense des intégristes, mais ils ont un argument en moins).

if (!$obj) {
    $result = null;
} else {
    $result = $obj->method1();
    if ($result) {
        $result = $result->attr1;
        if ($result) {
            $result = $result->method2();
        }
    }
}

// équivalent à tout le code ci-dessus
$result = $obj?->method1()?->attr1?->method2();

throw devient une expression

Auparavant, throw était un mot-clé du langage qui ne pouvait être utilisé que dans certaines situations. C’est désormais une expression, ce qui facilite son utilisation.
Par exemple, on peut maintenant écrire ceci :

$nom = $_POST['nom'] ?? throw new Exception("Mauvais paramètre");

Fonctions str_contains(), str_starts_with() et str_ends_with()

Ces trois fonctions permettent de savoir si une chaîne en contient un autre, si une chaîne commence par une autre, ou finit par une autre.
Ça a l’air bête, mais c’est infiniment plus lisible que d’utiliser str_pos() ou substr() comme on le faisait avant.

Promotion de constructeur

Cette fonctionnalité permet d’indiquer qu’un paramètre du constructeur doit être recopié dans un attribut de l’objet, juste en précisant la visibilité de l’attribut devant le type du paramètre. Et en faisant ça, on n’a même plus besoin de déclarer l’attribut.
Là, je dois dire que je ne m’y suis pas encore habitué. J’ai commencé à l’utiliser, mais ce n’est pas encore systématique. J’aime bien le fait de déclarer explicitement les attributs des objets, je trouve ça plus lisible que d’aller regarder dans les paramètres du constructeur. Mais on économise pas mal de lignes de code.

// avant PHP 8
class Toto {
private $a;
protected $b;
public function __construct(int $a, string $b) {
$this->a = $a;
$this->b = $b;
}
}
// à partir de PHP 8
class Toto {
public function __construct(private int $a, protected string $b) {
}
}

Le futur

PHP 8.1 va arriver en novembre 2021. Pas mal de nouvelles fonctionnalités vont apparaître à leur tour.

Personnellement, ce que j’attends le plus de cette version est l’arrivée des énumérations. Oui, je parle bien de la fonctionnalité qui existe dans le C depuis la nuit des temps, qui a mis 10 ans à arriver dans le Java, 25 ans à arriver dans le Python, et donc aussi 25 ans à arriver dans le PHP (on l’attend encore dans le Perl et le Ruby…).
Les énumérations, ça permet de rendre son code vraiment beaucoup plus propre dans certains cas. Ça évite d’utiliser des chaînes de caractères ou des nombres en constantes, avec toutes les erreurs de comparaison qui en découle (parce que les comparaisons se font sur les valeurs uniquement et pas sur le type).

La propriété readonly pourra être utilisée avec les attributs d’objets. Ainsi, un attribut (dont la valeur ne change plus une fois qu’elle est affectée) pourra être laissé en accès public, mais en lecture seule, évitant d’avoir à écrire un getter juste pour lire la valeur.

La fonction array_is_list() permettra de savoir facilement si un tableau est utilisé comme une simple liste numérique ou non. Là encore, c’est une aide mineure, mais qui peut faire gagner du temps en quelques rares occasions.

Les fibres (“fibers”) permettront de faire du code asynchrone en PHP. Là, j’attends de jouer avec avant de me faire une opinion.

5 commentaires pour “De PHP 7 à PHP 8, retour sur cinq ans d’innovation

  1. Mais pourquoi avoir rajouté ce « case » dans les enum. Ca n’a aucun sens , ca va les rendre pénible à écrire … Autant continuer à écrire une classe abstraite pleine de const ca revient au même 🙁

  2. Très bon résumé de toutes les dernières fonctionnalités dont beaucoup que je n’utilise pas encore…

  3. @Grunk : Bah les mots-clés « case » ne sont pas si dérangeants que ça. En tout cas, pas au point de dire que ça devient pénible à écrire.
    Parce que ta classe pleine de constantes, c’est bien beau, mais ça veut dire qu’au final tu va quand même passer des valeurs scalaires (chaîne ou entier) en paramètre de tes fonctions, avec tous les risques que ça implique.

    Exemple :
    class UserRightsEnum {
        public const NO_ACCESS = 0;
        public const READ_ONLY = 1;
        public const READ_WRITE = 2;
        public const ADMIN = 3;
    }
    class User {
        public function setRights(int $access) {
            // ...
        }
    }

    // ...
    $rights = UserRightsEnum::ADMIN;
    $user->setRights($rights); // ça marche
    $rights = 25;
    $user->setRights($rights); // ça passe alors qu'il ne faudrait pas !

    Alors qu’avec les énumérations, ton code est blindé parce que tu bénéficies de la vérification de type du langage.
    enum UserRights {
        case NO_ACCESS;
        case READ_ONLY;
        case READ_WRITE;
        case ADMIN;
    }
    class User {
        public function setRights(UserRights $access) {
            // ...
        }
    }

    // ...
    $rights = UserRights::ADMIN;
    $user->setRights($rights); // ça marche
    $rights = 25;
    $user->setRights($rights); // erreur de type

  4. @Amaury , on est d’accord , mais je pense qu’on est d’accordpour dire que opter pour la syntaxe C++/JAVA aurait été plus simple que faire comme swift :


    enum UserRights {
    NO_ACCESS;
    READ_ONLY;
    READ_WRITE;
    ADMIN;
    }

    Le case , ici n’apporte aucun intérêt surtout quand la syntaxe sans ce case est éprouvée depuis des années dans des langages de référence dont php s’inspire déjà beaucoup.

  5. @Grunk : oui, oui, mais bon… il y a tellement de bizarreries dans tous les langages, je ne vais pas m’offusquer pour si peu. C’est déjà bien que les énumérations arrivent en PHP.

    En espérant que les gens les utilisent : j’ai vu tellement de code C écrit par des gens connus et reconnus, qui utilisent encore des constantes là où il faudrait de toute évidence des typedef enum… alors que c’est dans le langage depuis 1989 !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.