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 типове

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)
{
	...
}

Обратно, autowiring в 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.

Последни публикации