PHP 8.0: Новини про типи даних (2/4)

3 рік тому Від David Grudl  

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

Давайте повернемося в історію. Введення підказок скалярних типів було значним проривом в PHP 7. Цього майже не сталося. Andreu Faulds, автор геніального рішення, яке було повністю зворотньо сумісним і необов'язковим завдяки declare(strict_types=1), був жорстко відкинутий спільнотою. На щастя, Ентоні Феррара захистив її і пропозицію в той час, розпочав кампанію, і RFC пройшов дуже близько. Ого. Більшість новин в PHP 8 належать легендарному Микиті Попову, і всі вони пройшли голосування, як ніж крізь масло. Світ змінюється на краще.

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

Типи об'єднань

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

class Button
{
	private string|object $caption;

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

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

Ви не можете використовувати псевдотипи void та mixed в об'єднаннях, оскільки це не має сенсу.

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

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

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

Методи getParameterType(), getReturnType() та getPropertyType() у Nette\Utils\Reflection генерують виключення у випадку типу об'єднання (це у версії 3.1; у попередній версії 3.0 ці методи повертають нульове значення для збереження сумісності).

mixed

Псевдотип mixed приймає будь-яке значення.

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

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

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

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

false

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

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

Тому типу true не існує. Також не можна використовувати тільки false, або тільки false|null, або тільки bool|false.

static

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

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 ресурс image замінено на об'єкт 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.

Тип variance, contravariance, covariance

Принцип підстановки Ліскова (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
    {}
}

Типи об'єднань також можуть бути розширені в параметрах і звужені в значеннях, що повертаються:

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.