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

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

Вышла версия PHP 8.0. Она настолько насыщена новинками, как ни одна версия до этого. Их представление потребовало целых четыре отдельных статьи. В этой второй мы рассмотрим типы данных.

Вернемся в историю. Существенным прорывом PHP 7 стало введение скалярных type hint. Этого могло и не случиться. Сообщество грубо отвергло автора замечательного решения Андреа Фаулдс, которое благодаря declare(strict_types=1) было полностью обратно совместимым и необязательным. К счастью, тогда за нее и ее предложение заступился Энтони Феррара, запустил кампанию, и RFC с трудом прошел. Уфф. Большинство новинок в PHP 8 — заслуга легендарного Никиты Попова, и в голосовании они прошли как по маслу. Мир меняется к лучшему.

PHP 8 доводит типы до совершенства. Исчезнет абсолютное большинство phpDoc аннотаций @param, @return и @var в коде, их заменит нативная запись и, главное, проверка движком PHP. В комментариях останутся только описания структур вроде string[] или более сложные аннотации для PHPStan.

Union Types

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 готов к union типам. В Schema с ними разбирается Expect::from(), разбираются с ними и презентеры, так что вы можете использовать их, например, в методах render и action:

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 не существует и в будущем он введен не будет. Ресурсы (Resources) — это исторический реликт времен, когда в 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) гласит, что потомки класса и реализации интерфейса никогда не должны требовать больше и предоставлять меньше, чем родитель. То есть метод потомка не должен требовать больше аргументов или принимать для параметров более узкий диапазон типов, чем предок, и наоборот, не должен возвращать более широкий диапазон типов. Но может возвращать более узкий. Почему? Потому что иначе наследование вообще бы не работало. Функция хоть и приняла бы объект определенного типа, но не знала бы, какие параметры можно передавать методам и что они будут на самом деле возвращать, потому что любой потомок мог бы это нарушить.

Таким образом, в ООП действует правило, что потомок может:

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

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.