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

5 років тому від 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.

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.