PHP 8.0 : Types de données (2/4)

il y a 4 ans par David Grudl  

La version 8.0 de PHP est sortie. Elle est tellement pleine de nouveautés qu'aucune version précédente ne l'a été. Leur présentation a nécessité quatre articles distincts. Dans ce deuxième, nous allons examiner les types de données.

Revenons à l'histoire. La percée majeure de PHP 7 a été l'introduction des type hints scalaires. Cela a failli ne pas se produire. L'auteure de la solution géniale Andreu Faulds, qui était entièrement rétrocompatible et optionnelle grâce à declare(strict_types=1), a été méchamment rejetée par la communauté. Heureusement, Anthony Ferrara a pris sa défense et celle de sa proposition à l'époque, a lancé une campagne et le RFC est passé de justesse. Ouf. La plupart des nouveautés de PHP 8 sont dues au légendaire Nikita Popov et elles sont passées comme une lettre à la poste lors du vote. Le monde s'améliore.

PHP 8 perfectionne les types. La grande majorité des annotations phpDoc @param, @return et @var disparaîtront du code et seront remplacées par une notation native et surtout par un contrôle par le moteur PHP. Seules les descriptions de structures comme string[] ou des annotations plus complexes pour PHPStan resteront dans les commentaires.

Types Union

Les types Union sont une énumération de deux ou plusieurs types qu'une valeur peut prendre :

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

PHP connaissait déjà certains types d'union spéciaux auparavant. Par exemple, les types nullable comme ?string, qui est l'équivalent du type d'union string|null, et la notation avec point d'interrogation peut être considérée comme un simple raccourci. Bien sûr, cela fonctionne aussi en PHP 8, mais il ne peut pas être combiné avec des barres verticales, donc au lieu de ?string|object, il faut écrire string|object|null en entier. De plus, iterable a toujours été l'équivalent de array|Traversable. Vous serez peut-être surpris d'apprendre que float est en fait aussi un type d'union qui accepte int|float, mais qui le convertit en float.

Les pseudo-types void et mixed ne peuvent pas être utilisés dans les unions, car cela n'aurait aucun sens.

Nette est prête pour les types union. Dans Schema, Expect::from() les accepte, et les présentateurs les acceptent également. Vous pouvez les utiliser dans les méthodes render et action, par exemple :

public function renderDetail(int|array $id)
{
	...
}

En revanche, l'autowiring dans Nette DI refuse les types union. Il manque pour l'instant un cas d'utilisation où il serait logique qu'un constructeur accepte, par exemple, tel ou tel objet. Bien sûr, s'il apparaît, il sera possible d'adapter le comportement du conteneur en conséquence.

Les méthodes getParameterType(), getReturnType() et getPropertyType() dans Nette\Utils\Reflection lèvent une exception dans le cas d'un type union (dans la version 3.1, dans l'ancienne version 3.0, elles renvoient null pour des raisons de compatibilité).

mixed

Le pseudo-type mixed indique que la valeur peut être absolument n'importe quoi.

Dans le cas des paramètres et des propriétés, il s'agit en fait du même comportement que si nous n'indiquions aucun type. À quoi sert-il alors ? Pour pouvoir distinguer quand le type est simplement manquant et quand il est vraiment mixed.

Dans le cas de la valeur de retour d'une fonction ou d'une méthode, ne pas indiquer de type diffère de l'indication du type mixed. Il s'agit en fait de l'opposé de void, car il indique que la fonction doit retourner quelque chose. L'absence de return est alors une erreur fatale.

En pratique, vous devriez l'utiliser rarement, car grâce aux types union, vous pouvez spécifier la valeur plus précisément. Il est donc utile dans des situations exceptionnelles :

function dump(mixed $var): mixed
{
	// afficher la variable
	return $var;
}

false

Le nouveau pseudo-type false ne peut être utilisé que dans les types union. Il a été créé pour décrire nativement le type de la valeur de retour des fonctions natives qui, historiquement, renvoient false en cas d'échec :

function strpos(string $haystack, string $needle): int|false
{
}

Pour cette raison, il n'existe pas de type true, on ne peut pas non plus utiliser false seul, ni false|null ou bool|false.

static

Le pseudo-type static ne peut être utilisé que comme type de retour d'une méthode. Il indique que la méthode renvoie un objet du même type que l'objet lui-même (tandis que self indique qu'elle renvoie la classe dans laquelle la méthode est définie). Ce qui est parfaitement adapté pour décrire les interfaces fluides :

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Ce type n'existe pas en PHP 8 et ne sera pas non plus introduit à l'avenir. Les ressources sont une relique historique de l'époque où PHP n'avait pas encore d'objets. Progressivement, les ressources seront remplacées par des objets et ce type finira par disparaître complètement. Par exemple, PHP 8.0 remplace la ressource représentant une image par l'objet GdImage et la ressource de connexion curl par l'objet CurlHandle.

Stringable

Il s'agit d'une interface que chaque objet doté de la méthode magique __toString() implémente automatiquement.

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

Dans la définition de la classe, il est possible d'indiquer explicitement class Email implements Stringable, mais ce n'est pas nécessaire.

Ce style de nommage est également reflété par Nette\Utils\Html, qui implémente l'interface Nette\HtmlStringable au lieu de l'ancienne IHtmlString. Les objets de ce type ne sont alors, par exemple, pas échappés par Latte.

Variance de type, contravariance, covariance

Le principe de substitution de Liskov (Liskov Substitution Principle – LSP) stipule que les descendants d'une classe et les implémentations d'une interface ne doivent jamais exiger plus et fournir moins que le parent. C'est-à-dire que la méthode d'un descendant ne doit pas exiger plus d'arguments ou accepter une plage de types plus étroite pour les paramètres que l'ancêtre, et inversement, elle ne doit pas renvoyer une plage de types plus large. Mais elle peut renvoyer une plage plus étroite. Pourquoi ? Parce que sinon, l'héritage ne fonctionnerait pas du tout. Une fonction accepterait certes un objet d'un certain type, mais elle ne saurait pas quels paramètres peuvent être passés aux méthodes et ce qu'elles retourneront réellement, car n'importe quel descendant pourrait le perturber.

Donc, en POO, il est vrai que le descendant peut :

  • accepter une plage de types plus large dans les paramètres (c'est ce qu'on appelle la contravariance)
  • renvoyer une plage de types plus étroite (covariance)
  • et les propriétés ne peuvent pas changer de type (elles sont invariantes)

PHP sait faire cela depuis la version 7.4 et tous les types nouvellement introduits en PHP 8.0 prennent également en charge la contravariance et la covariance.

Dans le cas de mixed, le descendant peut restreindre la valeur de retour à n'importe quel type, mais pas void, car il ne s'agit pas d'un type de valeur, mais de son absence. Le descendant ne peut pas non plus ne pas indiquer de type, car cela autorise également l'absence.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

Les types union peuvent également être étendus dans les paramètres et restreints dans les valeurs de retour :

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

De plus, false peut être étendu à bool dans un paramètre, ou inversement, bool peut être restreint à false dans la valeur de retour.

Toutes les infractions à la covariance/contravariance entraînent une erreur fatale en PHP 8.0.

Dans les prochaines parties, nous verrons ce que sont les attributs, quelles nouvelles fonctions et classes sont apparues en PHP, et nous présenterons le Just in Time Compiler.