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

3 года назад от David Grudl  

PHP версии 8.0 только что был выпущен. Она полна новых возможностей, как ни одна из предыдущих версий. Их введение заслуживает четырех отдельных статей. Во второй части мы рассмотрим типы данных.

Давайте вернемся в историю. Введение подсказок скалярных типов было значительным прорывом в PHP 7. Этого почти не произошло. Андреу Фолдс, автор гениального решения, которое было полностью обратно совместимым и необязательным благодаря 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() принимает их, и presenters также принимает их. Вы можете использовать их, например, в методах render и action:

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

С другой стороны, autowiring в Nette DI отвергает типы union. Пока что не существует случая, когда конструктору имело бы смысл принимать либо один, либо другой объект. Конечно, если такой случай появится, можно будет соответствующим образом скорректировать поведение контейнера.

Методы getParameterType(), getReturnType() и getPropertyType() в Nette\Utils\Reflection выбрасывают исключение в случае типа union (это в версии 3.1; в более ранней версии 3.0 эти методы действительно возвращаются для поддержания совместимости с null).

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.

Типовая дисперсия, контравариант, ковариация

Принцип замещения Лискова (LSP) гласит, что классы расширения и реализации интерфейсов никогда не должны требовать больше и предоставлять меньше, чем родитель. То есть дочерний метод не должен требовать больше аргументов или принимать более узкий диапазон типов для параметров, чем родительский, и наоборот, он не должен возвращать более широкий диапазон типов. Но он может возвращать меньше. Почему? Потому что в противном случае наследование нарушится. Функция будет принимать объект определенного типа, но она не будет иметь представления о том, какие параметры она может передавать своим методам и какие типы они будут возвращать. Любой ребенок может сломать ее.

Поэтому в ООП дочерний класс может:

  • принимать более широкий диапазон типов в параметрах (это называется контравариант)
  • возвращать более узкий диапазон типов (ковариация)
  • а свойства не могут менять тип (они инвариантны).

PHP умеет это делать с версии 7.4, и все новые типы, появившиеся в PHP 8.0, также поддерживают contravariance и covariance.

В случае с 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..

Последние сообщения