PHP 8.0: Типи даних (2/4)

4 роки тому від David Grudl  

Вийшла версія PHP 8.0. Вона настільки насичена новинками, як жодна версія до неї. Їх представлення вимагало аж чотирьох окремих статей. У цій другій ми розглянемо типи даних.

Повернемося до історії. Суттєвим проривом PHP 7 стало введення скалярних підказок типів. Майже до цього не дійшло. Авторку чудового рішення Andreu Faulds, яке завдяки declare(strict_types=1) було повністю зворотно сумісним і необов'язковим, спільнота грубо відкинула. На щастя, за неї та її пропозицію тоді заступився Anthony Ferrara, розпочав кампанію, і RFC дуже тісно пройшов. Уффф. Більшість новинок у PHP 8 належить легендарному Nikita Popov, і в голосуванні вони пройшли як по маслу. Світ змінюється на краще.

PHP 8 доводить типи до досконалості. Зникне абсолютна більшість phpDoc анотацій @param, @return та @var у коді, їх замінить нативний запис і, головне, перевірка рушієм PHP. У коментарях залишаться лише описи структур, таких як string[], або складніші анотації для PHPStan.

Типи Union

Типи Union – це перелік двох або більше типів, яких може набувати значення:

class Button
{
	private string|object $caption;

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

Деякі спеціальні типи union PHP знав і раніше. Наприклад, nullable типи, такі як ?string, що є еквівалентом типу union string|null, і запис зі знаком питання можна вважати лише скороченням. Зрозуміло, він працює і в PHP 8, але його не можна комбінувати з вертикальними рисками, тобто замість ?string|object потрібно писати повне string|object|null. Далі, iterable завжди був еквівалентом array|Traversable. Можливо, вас здивує, що типом union є власне і float, який насправді приймає int|float, проте перетворює на float.

У типах union не можна використовувати псевдотипи void та mixed, оскільки це не мало б жодного сенсу.

Nette готова до роботи з об'єднаними типами. У Schema Expect::from() приймає їх, і презентатори також приймають їх. Ви можете використовувати їх, наприклад, у методах рендерингу та дії:

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

Натомість автовайринг у Nette DI відхиляє типи union. Поки що бракує use case, де б мало сенс, щоб, наприклад, конструктор приймав або той, або інший об'єкт. Звичайно, якщо він з'явиться, можна буде відповідно змінити поведінку контейнера.

Методи getParameterType(), getReturnType() та getPropertyType() в Nette\Utils\Reflection викидають виняток у випадку типу union (у версії 3.1, у старішій 3.0 повертають null через сумісність).

mixed

Псевдотип mixed говорить, що значення може бути абсолютно будь-яким.

У випадку параметрів та властивостей це, власне, та сама поведінка, як коли ми не вказуємо жодного типу. То для чого він потрібен? Щоб можна було розрізнити, коли тип просто відсутній, а коли він дійсно mixed.

У випадку поверненого значення функції та методу невказання типу відрізняється від вказання типу mixed. Це, власне, протилежність void, оскільки говорить, що функція повинна щось повернути. Відсутній return тоді є фатальною помилкою.

На практиці ви повинні використовувати його рідко, оскільки завдяки типам union ви можете точніше вказати значення. Тому він підходить у виняткових ситуаціях:

function dump(mixed $var): mixed
{
	// вивести змінну
	return $var;
}

false

Новий псевдотип false можна, навпаки, використовувати лише в типах union. Він виник з потреби нативно описати тип поверненого значення для нативних функцій, які історично у випадку невдачі повертають false:

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

З цієї причини не існує типу true, не можна використовувати також саме false або false|null чи bool|false.

static

Псевдотип static можна використовувати лише як тип повернення методу. Він говорить, що метод повертає об'єкт того ж типу, що й сам об'єкт (тоді як self говорить, що повертає клас, у якому визначено метод). Що чудово підходить для опису fluent interfaces:

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

Цей тип не існує в PHP 8 і в майбутньому вводитися не буде. Ресурси є історичним реліктом з часів, коли PHP ще не мав об'єктів. Поступово ресурси будуть замінюватися об'єктами, і з часом цей тип повністю зникне. Наприклад, PHP 8.0 замінює ресурс, що представляє зображення, на об'єкт GdImage, а ресурс з'єднання curl — на об'єкт CurlHandle.

Stringable

Це інтерфейс, який автоматично реалізує кожен об'єкт з магічним методом __toString().

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

function print(Stringable|string $s)
{
}

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

У визначенні класу можна явно вказати class Email implements Stringable, але це не обов'язково.

Цей стиль іменування відображає і Nette\Utils\Html, яке реалізує інтерфейс Nette\HtmlStringable замість попереднього IHtmlString. Об'єкти цього типу, наприклад, Latte не екранує.

Варіантність типів, контраваріантність, коваріантність

Принцип заміщення Лісков (Liskov Substitution Principle – LSP) говорить, що нащадки класу та реалізації інтерфейсу ніколи не повинні вимагати більше і надавати менше, ніж батько. Тобто, метод нащадка не повинен вимагати більше аргументів або приймати для параметрів вужчий діапазон типів, ніж предок, і навпаки, не повинен повертати ширший діапазон типів. Але може повертати вужчий. Чому? Тому що інакше успадкування взагалі б не працювало. Функція б хоч і прийняла об'єкт певного типу, але не знала б, які параметри можна передавати методам і що вони насправді повертатимуть, оскільки будь-який нащадок міг би це порушити.

Отже, в ООП діє правило, що нащадок може:

  • у параметрах приймати ширший діапазон типів (це називається контраваріантністю)
  • повертати вужчий діапазон типів (коваріантність)
  • а властивості не можуть змінювати тип (вони інваріантні)

PHP це вміє з версії 7.4, і всі нововведені типи в PHP 8.0 також підтримують контраваріантність та коваріантність.

У випадку mixed нащадок може звузити повернене значення до будь-якого типу, проте не void, оскільки це не тип значення, а його відсутність. Також нащадок не може не вказати тип, оскільки це також допускає відсутність.

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

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

Також типи union можна в параметрах розширювати, а в повернених значеннях звужувати:

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

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

Далі, false може бути в параметрі розширено до bool, або навпаки, bool у поверненому значенні звужено до false.

Усі порушення коваріантності/контраваріантності призводять у PHP 8.0 до фатальної помилки.

У наступних частинах ми покажемо, що таке атрибути, які нові функції та класи з'явилися в PHP, і представимо Just in Time Compiler.