Aller au contenu
AFUP AFUP Day 2025 Baromètre Planète PHP PUFA
 

Typage en PHP comment ça fonctionne ?

Description

PHP est un langage qui a été conçu sans système de typage mais dont un a été introduit au fur et à mesure.

Dans ce talk nous ferons une exploration en profondeur du système de typage de PHP: 

  • Les différents types disponibles
  • La représentations des types en internes 
  • Comment PHP valide et coerce les types lors de l'exécution
  • Comment PHP assure la compatibilité des types entres classes enfants et parentes (Liskov Substitution Principle)
Conférence donnée lors du Forum PHP 2022, ayant eu lieu les 13 et 14 octobre 2022.

Informations complémentaires

Vidéo

Le speaker

George BANYARD

George est actuellement sous contrat à temps partiel avec la fondation PHP pour maintenir PHP. Il participe aussi à la documentation et il est le mainteneur principal de la documentation française en ce moment. Il vient de terminer son Bachelor en math pure à l'Imperial College London.

Verbatim

_ Bonjour. C'est ma première conf. Si c'est un peu nerveux, je m'en excuse. Je suis ravi de voir une salle complète, quasiment. Ce sera sur le typage en PHP et on va voir comment ça fonctionne. Sur Internet, je m'appelle Girgias. J'ai étudié les maths pures. Je suis un core dev de PHP. Si vous trouvez des erreurs, c'est parce que je ne sais pas écrire ! Je suis au-dessus auteur de pas mal de RSC. Si vous voulez vous faire du mal, c'est là où il faut aller. Je pense qu'on peut commencer. Voilà ce qu'on va faire à peu près. On va faire un peu de maths. Et on voit voir aussi du C. J'espère que vous êtes prêts. On va commencer par la terminologie des symboles. Un système de type est un système logique constitué d'un ensemble de règles qui assignent la propriété appeler un type à chaque terme. Généralement, les termes sont une construction d'un langage de prod. Ça va être les variables, les fonctions. Ça dicte les opérations qui peuvent être utilisées sur un terme. Voilà quelques symboles. Le A inversé pour tout, le E inversé pour il existe. U est un sous-type de V ou X un super-type de Y. On a quelques types fondamentaux où on a un type universel qui ressemble à un T. On a un type unitaire qui est juste un type qui a qu'une seule valeur. Je vois que mes slides sont trop longues et que c'est couvert par le texte. Le dernier type, c'est le type vide. Le système de type deux PHP. En PHP, paramètres de fonction, la valeur de retour d'une fonction d'une méthode, ou une propriété d'une classe peuvent définir un type. Il y a les types primitifs, les types définis en espace utilisateur, les types libéraux, les types callable, les types composés et les alias de type. On a le type universel mixed. On a le type objet, le type tableau, on a les types scalaires. On a les types unitaires null. Avant PHP 8.2, vous ne pouviez vous en servir que dans certains types d'union.

Il y a aussi les types vide never. Ça veut dire que la fonction ne retombe jamais. Soit elle enchaîne les exceptions, soit c'est une boucle infinie. En PHP, toutes fonctions retournent une valeur. Même si vous tapez une fonction comme void, vous pouvez récupérer le résultat. Vous aurez null. C'est plus une information pour vous dire que ça ne sert à rien, la valeur de retour. Il y a les class-types, ce sont les interfaces, les classes et les énumérations qui existent depuis PHP 8.1. Il y a self, la classe en question dans laquelle vous êtes, il y a parent et static. Cela spécifie que la méthode retourne l'instance sur laquelle la méthode est appelée. Pas juste une instance parente qui peut être self, mais une instance spécifique. On a des types littéraux. Ce sont des types qui sont des sous-types concrets d'un type. C'est une valeur. Il existe false depuis PHP 8.0. On ne pouvait s'en servir que dans un type d'union. À partir de PHP 8, on a aussi true. Vous ne pouvez pas définir un type littéral en espace utilisateur. Dans PHP, on va vous dire d'utiliser une énumération à la place. Callable, je ne sais pas si vous connaissez l'enfer que c'est en PHP. C'est beaucoup de choses. C'est une chaîne de caractères qui correspond à un nom de fonction, strlen. C'est une paire dans un tableau, une instance, un nom de méthode. Ça aussi s'appelait la méthode statique. C'est un objet qui implémente invoke. Où ça peut être une closure, récupérable avec la syntaxe strlen (...). On ne peut pas définir une propriété de classe comme callable à cause de diverses raisons. On a des types composés depuis PHP 8. Des types d'intersection depuis PHP8.1 qui demandent que la classe ou l'objet en question implémente deux interfaces. Les types d'unions simples depuis PHP 8. Vous dites que vous avez besoin d'un objet T ou U, etc. Depuis PHP 8.2 on a les types FDN. Ce sont les Formes Normales Disjonctives. Une Forme Normale Disjonctive, ou FDN, c'est une normalisation d'une expression logique qui est une disjonction de clauses conjonctives. Vous suivez ! Qu'est-ce que ça veut dire ? C'est une liste d'u ça, ou ça, ou ça... Vous ne pouvez pas faire et, ou...

À partir de PHP 8.2, iterable, ça devient un alias de type résolu lors de la compilation. Avant, c'était un pseudo type primitif. Il est impossible de définir un alias en espace de type utilisateur. Ça ne fait pas encore trop peur ? Chaque valeur en PHP est représentée par une Zval. Pour indiquer ce que cette valeur contient, il y a un champ qui est le type info. Il peut avoir plein de valeurs différentes. IS_UNDEF, IS_NULL, etc. La vérification d'un type d'objet, si vous avez besoin de vérifier quelque chose. Dans un objet PHP, il y a une zend_class_entry. Ça contient le type, le nom et si c'est une classe parente. Ça contient aussi certains flags, le nombre d'interfaces que cette classe implémente et la liste des interfaces. L'implémentation d'instanceof est séparée en deux. Ça checke c'est la classe que vous passez est la même que la classe que vous voulez vérifier que c'est une instanceof. On va d'abord vérifier que la classe en question est une interface. Dans ce cas, on boucle sur les interfaces implémentées et on vérifie que ça existe dans la liste. Sinon, on boucle sur les parents. On vérifie que c'est en mode OK. Si on arrive sur le pointeur null, ça veut dire que ce n'est pas une instance. Les types en interne sont représentés par la structure zend_typ.

C'est défini avec un bloc de mémoire en C et un type_mask. Une des infos qu'il peut contenir, c'est pour savoir s'il y a des types primitifs. Dans le pointeur, il peut y avoir deux choses : le nom d'une classe si le type ne contient qu'un seul nom de classe, soit une zend_typ_list. Les unions, c'est la même chose. C'est comme ça que des fois, vous avez des unions imbriquées. On voit qu'il y a un autre type de liste. J'ai l'impression d'aller très vite. L'héritage et le principe de substitution de Liskov, je pense que vous en avez entendu parler. On appelle ça le LSP. Le principe de substitution de Liskov est en programmation orientée objet, une définition particulière de la notion de ce type. Il a été formulé par Barbara Liskov et Jeannette Wing en 1993. La formule condensée est la suivante. LSP est un principe à propos du remplacement d'un type par rapport à un autre, tel que les interactions avant et après ne soient pas perturbées. LSP n'a rien à voir avec la classe en elle-même. C'est comment vous allez utiliser une méthode, une fonction et une classe tel que les utilisateurs avant et après ne soient pas affectés. Les préconditions ne peuvent pas être renforcées dans un sous-type. Si vous avez votre type de base qui est au milieu, vous ne pouvez que le remplacer par quelque chose d'en haut et qui retourne quelque chose de plus spécifique en retour. Les méthodes ne peuvent pas ajouter des paramètres obligatoires. Si vous utilisez une méthode de la classe parente, si vous la changez par un sous-type, il manque des paramètres. Le type des paramètres des méthodes doit être contra-variant. Le type de retour des méthodes doit être co-variant. Il doit être plus spécifique.

Le type des propriétés doit être co et contra-variant. Pour vérifier tout ça, on a besoin d'une seule fonction : regardait que le type soit co-variant. Fe représente le sous-type et proto le super-type. Comment tout cela s'est fait ? Il est beau, le code C ! On va regarder les types primitifs. À part de void, tout est co-variant au type universel mixte. Le type primitif peut être supprimé pour être co-variant. Grosso modo, on vérifie qu'il y a des types qui sont ajoutés ou pas. Si un type change self en static, c'est un sous-type de self. Si on ajoute le type never, tout va bien aussi. Mais si on ajoute d'autres types, c'est vraiment qu'on n'est pas co-variant, donc c'est une erreur. Ah, les maths ! C'est bien ! Là, on va essayer de voir comment on peut déterminer si dans un type d'union, si U est un sous-type de V. Tous les types qui font partie du type d'union, on va les appeler les Ui. La première ligne, si pour chaque type, Ui, il existe un type Vi, tel que Ui est un sous-type de Vj, alors le type d'union Ui jusqu'à est sous-type de type d'union de V1 jusqu'à Vm. Deuxième ligne. Si pour tout type Ui, pour tous les types Vi... Je vous fais des maths et je fais des erreurs dans les slides. Si on a une erreur, c'est... Si on quitte les fonctions en avance, ça veut dire que c'est une erreur d'héritage et qu'on n'est pas co-variant.

On checke si notre type est un type d'intersection. Dans ce cas, on va juste demander si ce type d'intersection est un sous-type de nos types parents. Sinon, c'est dans le cas où on est dans un type d'union. On va boucler sur les classes. Dès qu'on trouve que la classe est un sous-type des parents, alors tout va bien. Dès qu'on a un cas qui n'est pas valide, on sort. Sinon, tout va bien et l'héritage peut être validé vu que U est un sous-type de V. Maintenant, c'est encore plus compliqué. J'espère que j'arrive à condenser ça en 5 minutes. Un type d'intersection en PHP est forcément un objet. On ne peut pas demander à ce qu'un entier implémente l'interface traversable. Déjà, ça nous aide que si on demande qu'un type d'intersection est un sous-type d'un objet, alors tout va bien. Dans le cas général, s'il existe Vj et un type Ui tel qu'Ui est un sous-type de Vj, alors le type l'intersection U1 jusqu'à Un est un sous-type de V1 jusqu'à Vm. On doit vérifier que l'un des types est un sous-type de l'un des types d'union. Pour le cas où l'on veut vérifier qu'un type d'intersection est un sous-type d'un autre type d'intersection, il faut que pour tous les types Vj, il existe un type Ui tel qu'Ui est un sous-type de Vj. On peut ajouter des intersections. Si je demande qu'un type soit à traversable... Le cas qui rend ceci plus compliqué, c'est les quantificateurs. C'est le "il existe". On ne peut pas les inverser facilement. Ça ne veut pas dire la même chose. On doit d'abord itérer sur les types de V. Il suffit d'une seule paire I ou J pour que ce soit un sous-type. Ce n'est pas facile. Comparé au cas d'avant, ce n'est pas pareil. On va passer à travers à chaque type. On va vérifier que le type U que l'on a de la liste, c'est un type d'intersection. Relire du code C en live, ce n'est pas facile. Je ne vous aime vraiment pas ! On va récupérer le nom de la classe. On va vérifier si elle existe. Ça peut arriver. Toutes les classes sont définies en avance, non, l'autoloading. Le statut, on va vérifier si c'est valide, si c'est équivalent aux statuts qu'on a pour chaque validation de chaque sous-type, si ça correspond. Le futur d'un système de typage en PHP ? Il peut y avoir les alias de type en espace utilisateur. Il peut y avoir les types de fonctions.

Callable, c'est un peu un type de fonction, mais c'est très générique. Ça ne fait pas de spécificité sur les paramètres. Si je voulais que ma fonction prenne en paramètre un entier et qu'elle retourne dans le Booléen... Les types génériques, je suppose que vous savez ce que c'est. C'est un type déterminé en run time. Le cas typique, c'est une collection. Vous n'êtes pas obligés de créer une place pour gérer une collection. Si vous faites une classe, à chaque fois que vous passez un élément, le type T va être prouvé... Le dernier cas dans le futur, les paramètres in-out. Ça ressemble un peu aux références. Les références en PHP, ça n'accepte que le type en entrée. Si vous demandez un tableau en référence, vous pouvez lui passer un tableau, mais la fonction peut changer votre référence de type. Par exemple si on changeait le in-out pour une esperluette, Si je changeais le type entier en V, ça marcherait en partant du principe que c'est un tableau. Les paramètres in-out pourraient garantir cela. Si je changeais le type, alors j'aurais une erreur. Je pense que j'ai le temps. On va passer à la coercition de types scalaires. Par défaut, PHP coerce les valeurs scalaires. Si vous avez un int, je vais essayer de le convertir, sinon, je vais voir si je peux le convertir en float, sinon je regarde si c'est convertible en string ou en booléen. C'est déprécié depuis le PHP 8.1. Les fonctions internes font aussi la coercition de null. Strict_types, qui connaît ? Qui pense savoir ce que ça fait ? Ça ne fait plus de coercition pour les types scalaires dans les cas suivants uniquement. L'appel de fonction dans le fichier qui définit strict_types, la valeur de retour pour une fonction qui définit en espace utilisateur et l'assignation des valeurs à une propriété typée. Ce sont les seuls cas. Ça ne change pas le comportement des opérateurs binaires. Il y a aussi les constructions de langage. Si vous faites exit(true), ça convertit ça comme une chaîne de caractères. Si vous envoyez une fonction comme filtre, ça va ignorer le fait que vous avez strict_types. Voilà. Merci beaucoup. J'espère que ce n'était pas trop dur !

_ Merci beaucoup, George. On a le temps pour prendre quelques questions. Si vous en avez, on vient vers vous.

_ Il y a quand même quelques questions ! Je pensais que j'allais prendre plus de temps.

_ Salut. Tu as parlé des generics dans le futur. Il y a une RFC en cours qui date de 2016. Ça ne fait pas très futur.

_ Une RFC, ça ne veut rien dire. Une RFC, c'est un document qui détaille une proposition et qui a une implémentation avec. C'est un proposal. Ce n'est pas pourtant que c'est dans le langage. Le problème avec les génériques, c'est l'implémentation. Tout le monde le veut, moi compris. Tout le monde veut les génériques. Le problème, c'est comment on fait une implémentation qui est rapide. Mon PHP, ce n'est pas un langage compilé où on compile une fois. Oui, ça existe depuis très longtemps, la RFC. Nikita a implémenté énormément de choses en PHP. Même lui a un peu abandonné l'idée. Je ne suis pas là pour dire que c'est compliqué, mais c'est compliqué ! J'espère que ça répond à la question.

_ Merci beaucoup pour cette conférence, même si les maths étaient un peu compliquées, ça fait plaisir de voir des trucs compliqués en conférence. J'ai une question par rapport au run time. J'ai noté une dégradation des performances d'une application lorsqu'elle a été migrée en PHP 7.4, lorsqu'on avait des types de propriétés. Il y avait des vérifications type qui ont été faites en run time et qui gardaient les performances. Est-ce qu'il y a un moyen pour optimiser ça et éviter qu'on perde dans les performances ?

_ Le typage en PHP peut apporter des optimisations. Dans PHP, on a décidé qu'on allait définir une classe par fichier. Ça a été perdu... Il compile le fichier, il fait les optimisations de perf, mais il oublie ce fichier. Pour ce cas-là, il n'y a pas de solution. Vous perdez le check au run time.

_ On a le temps pour une dernière question.

_ Bonjour. Merci. J'ai une question sur les génériques. On a bien compris que c'est compliqué. Est-ce que vous avez une opinion par rapport aux approches de la transpilation ? Comment tu vois ce sujet ?

_ Tu veux dire dans le sens où on oublie l'information que ce sont des génériques ?

_ Comme type_strict.

_ Je n'en fais pas beaucoup.

_ On code en PHP spécial. Après, on passe en transpilator.

_ C'est l'opinion que j'ai sur type script. PHP, c'est un peu un projet politique, avec leurs avis. Il n'y a pas vraiment de structure. Ça n'empêche pas quelqu'un en userland de faire un préprocesseur. Je crois que j'avais vu un projet de quelqu'un qui faisait un peu ça. Pour les génériques, il avait analysé tous les cas, les classes qui étaient passées dans une collection. Ils généraient avant les classes nécessaires et convertissait les classes pour utiliser les types. C'est possible en userland. On n'investit pas beaucoup de temps.

_ Merci pour la présentation.

_ Merci à vous de m'avoir invité. Si jamais, je suis là jusqu'à 19h.