PHP 8.0 : Nouveautés dans les types de données (2/4)

il y a 4 ans de David Grudl  

La version 8.0 de PHP vient d'être publiée. Elle est pleine de nouvelles fonctionnalités, comme aucune autre version auparavant. Leur présentation mérite quatre articles distincts. Dans la deuxième partie, nous allons nous intéresser aux types de données.

Remontons dans le temps. L'introduction des indices de type scalaire a été une avancée significative en PHP 7. Elle a failli ne pas voir le jour. Andreu Faulds, l'auteur de cette solution ingénieuse, entièrement rétrocompatible et optionnelle grâce à declare(strict_types=1), a été sévèrement rejeté par la communauté. Heureusement, Anthony Ferrara a pris sa défense et celle de la proposition à l'époque, a lancé une campagne et le RFC est passé de très près. Ouf. La plupart des nouveautés de PHP 8 sont l'œuvre du légendaire Nikita Popov et elles ont toutes passé le vote comme un couteau dans du beurre. Le monde change pour le mieux.

PHP 8 amène les types à la perfection. La grande majorité des annotations phpDoc comme @param, @return et @var seront remplacées par une notation native. Mais surtout, les types seront vérifiés par le moteur PHP. Seules les descriptions de structures telles que string[] ou les annotations plus complexes pour PHPStan resteront dans les commentaires.

Types d'union

Les types d'union sont une énumération de deux ou plusieurs types qu'une variable peut accepter :

class Button
{
	private string|object $caption;

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

Certains types d'union ont déjà été introduits en PHP. Les types nuls, par exemple. Comme ?string, qui est équivalent au type union string|null. La notation du point d'interrogation peut être considérée comme une abréviation. Bien sûr, elle fonctionne également en PHP 8, mais vous ne pouvez pas la combiner avec des barres verticales. Ainsi, au lieu de ?string|object, vous devez écrire string|object|null. Par ailleurs, iterable a toujours été équivalent à array|Traversable. Vous serez peut-être surpris de constater que float est également un type d'union, car il accepte les deux int|float, mais convertit la valeur en float.

Vous ne pouvez pas utiliser les pseudotypes void et mixed 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)
{
	...
}

D'autre part, le câblage automatique dans Nette DI rejette les types d'union. Jusqu'à présent, il n'y a pas de cas d'utilisation où il serait logique que le constructeur accepte un objet ou un autre. Bien sûr, si un tel cas d'utilisation 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 du type union (c'est-à-dire dans la version 3.1 ; dans la version 3.0 antérieure, ces méthodes reviennent pour maintenir la compatibilité nulle).

mixed

Le pseudotype mixed accepte n'importe quelle valeur.

Dans le cas des paramètres et des propriétés, il entraîne le même comportement que si nous ne spécifions aucun type. Alors, quel est son but ? A distinguer quand un type est simplement absent et quand il est intentionnellement mixed.

Dans le cas des valeurs de retour des fonctions et des méthodes, ne pas spécifier le type diffère de l'utilisation de mixed. C'est le contraire de void, car la fonction doit retourner quelque chose. Un retour manquant produit alors une erreur fatale.

En pratique, vous devriez rarement l'utiliser, car grâce aux types d'union, vous pouvez spécifier la valeur avec précision. Elle ne convient donc que dans des situations uniques :

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

false

Contrairement à mixed, vous pouvez utiliser le nouveau pseudotype false exclusivement dans les types d'union. Il est né de la nécessité de décrire le type de retour des fonctions natives, qui renvoient historiquement false en cas d'échec :

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

Par conséquent, il n'existe pas de type true. Vous ne pouvez pas non plus utiliser false seul, ni false|null, ni bool|false.

statique

Le pseudotype static ne peut être utilisé que comme type de retour d'une méthode. Il indique que la méthode retourne un objet du même type que l'objet lui-même (alors que self indique qu'elle retourne la classe dans laquelle la méthode est définie). Il est excellent pour décrire des 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();

ressource

Ce type n'existe pas en PHP 8 et ne sera pas introduit dans le futur. Les ressources sont une relique de l'époque où PHP n'avait pas d'objets. Progressivement, les ressources vont être remplacées par des objets. Finalement, elles disparaîtront complètement. Par exemple, PHP 8.0 remplace la ressource image par un objet GdImage, et la ressource curl par un objet CurlHandle.

Stringable

C'est une interface qui est automatiquement implémentée par chaque objet avec une méthode magique __toString().

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

function print(Stringable|string $s)
{
}

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

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

Nette\Utils\Html Latte reflète également ce schéma de dénomination en implémentant l'interface Nette\HtmlStringable au lieu de l'ancienne IHtmlString. Les objets de ce type, par exemple, ne sont pas échappés par Latte.

Type variance, contravariance, covariance

Le principe de substitution de Liskov (LSP) stipule que les classes d'extension et les implémentations d'interface ne doivent jamais exiger plus et fournir moins que le parent. En d'autres termes, la méthode enfant ne doit pas exiger plus d'arguments ou accepter une gamme plus étroite de types pour les paramètres que le parent, et vice versa, elle ne doit pas retourner une gamme plus large de types. Mais elle peut en renvoyer moins. Pourquoi ? Parce que sinon, l'héritage serait rompu. Une fonction accepterait un objet d'un type spécifique, mais elle n'aurait aucune idée des paramètres qu'elle peut passer à ses méthodes et des types qu'elles renverraient. N'importe quel enfant pourrait la casser.

Donc, dans la POO, la classe enfant peut :

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

PHP est capable de faire cela depuis la version 7.4, et tous les nouveaux types introduits en PHP 8.0 supportent également la contravariance et la covariance.

Dans le cas de mixed, l'enfant peut réduire la valeur de retour à n'importe quel type, mais pas void, car il ne représente pas une valeur, mais plutôt son absence. L'enfant ne peut pas non plus omettre une déclaration de type, car cela permet également une absence de valeur.

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

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

Les types d'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
    {}
}

En outre, false peut être étendu à bool dans le paramètre ou vice versa bool à false dans la valeur de retour.

Toutes les infractions à la covariance/contravariance conduisent à une erreur fatale en PHP 8.0.

Dans les prochaines parties de cette série, nous montrerons ce que sont les attributs, quelles nouvelles fonctions et classes sont apparues en PHP et nous présenterons Just in Time Compiler.