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

преди 3 години От David Grudl  

Току-що беше пусната версия 8.0 на PHP. Тя е пълна с нови функции, както никоя друга версия досега. Тяхното представяне заслужава четири отделни статии. Във втората част ще разгледаме типовете данни.

Нека се върнем назад в историята. Въвеждането на подсказки за скаларни типове беше значителен пробив в 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 е готов за съюзни типове. В Схемата 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.

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

Принципът на заместване на Лисков (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.

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