PHP 8.0: Tipuri de date (2/4)

acum 4 ani de David Grudl  

A fost lansată versiunea PHP 8.0. Este atât de plină de noutăți cum nu a mai fost nicio versiune înainte. Prezentarea lor a necesitat patru articole separate. În acest al doilea articol, vom analiza tipurile de date.

Să ne întoarcem în istorie. O descoperire fundamentală a PHP 7 a fost introducerea type hint-urilor scalare. Aproape că nu s-a întâmplat. Autoarea soluției uimitoare Andreu Faulds, care datorită declare(strict_types=1) era complet compatibilă invers și opțională, a fost respinsă urât de comunitate. Din fericire, atât ea, cât și propunerea ei au fost susținute atunci de Anthony Ferrara, care a lansat o campanie și RFC-ul a trecut la limită. Ufff. Majoritatea noutăților din PHP 8 sunt opera legendarului Nikita Popov și în votare i-au trecut ca unse. Lumea se schimbă în bine.

PHP 8 duce tipurile la perfecțiune. Vor dispărea marea majoritate a adnotărilor phpDoc @param, @return și @var din cod și vor fi înlocuite de notația nativă și, mai ales, de verificarea de către motorul PHP. În comentarii vor rămâne doar descrierile structurilor precum string[] sau adnotări mai complexe pentru PHPStan.

Tipuri Union

Tipurile Union sunt o enumerare a două sau mai multe tipuri pe care le poate lua o valoare:

class Button
{
	private string|object $caption;

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

PHP cunoștea deja unele tipuri union speciale. De exemplu, tipurile nullable precum ?string, care este echivalentul tipului union string|null, iar notația cu semn de întrebare poate fi considerată doar o prescurtare. Desigur, funcționează și în PHP 8, dar nu poate fi combinată cu bare verticale, deci în loc de ?string|object trebuie scris complet string|object|null. În plus, iterable a fost întotdeauna echivalentul array|Traversable. Poate vă surprinde faptul că și float este de fapt un tip union care acceptă int|float, însă convertește la float.

În union-uri nu se pot utiliza pseudotipurile void și mixed, deoarece acest lucru nu ar avea niciun sens.

Nette este pregătit pentru tipurile de uniune. În Schema, Expect::from() le acceptă, iar prezentatorii le acceptă și ei. Le puteți utiliza în metodele de redare și de acțiune, de exemplu:

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

În schimb, autowiring-ul în Nette DI refuză tipurile union. Lipsește deocamdată un caz de utilizare în care ar avea sens ca, de exemplu, un constructor să accepte fie un obiect, fie altul. Desigur, dacă apare, va fi posibilă adaptarea comportamentului containerului în consecință.

Metodele getParameterType(), getReturnType() și getPropertyType() din Nette\Utils\Reflection aruncă o excepție în cazul unui tip union (în versiunea 3.1, în versiunea mai veche 3.0 returnează null din motive de compatibilitate).

mixed

Pseudotipul mixed indică faptul că valoarea poate fi absolut orice.

În cazul parametrilor și proprietăților, este de fapt același comportament ca atunci când nu specificăm niciun tip. La ce este bun atunci? Pentru a putea distinge când tipul pur și simplu lipsește și când este într-adevăr mixed.

În cazul valorii returnate de funcție și metodă, nespecificarea tipului diferă de specificarea tipului mixed. Este de fapt opusul lui void, deoarece indică faptul că funcția trebuie să returneze ceva. Lipsa return-ului este atunci o eroare fatală.

În practică, ar trebui să-l utilizați rar, deoarece datorită tipurilor union puteți specifica valoarea mai precis. Se potrivește deci în situații excepționale:

function dump(mixed $var): mixed
{
	// afișează variabila
	return $var;
}

false

Noul pseudotip false poate fi utilizat, în schimb, doar în tipurile union. A apărut din nevoia de a descrie nativ tipul valorii returnate pentru funcțiile native, care istoric returnează false în caz de eșec:

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

Din acest motiv, nu există tipul true, nu se poate utiliza nici false singur sau false|null sau bool|false.

static

Pseudotipul static poate fi utilizat doar ca tip de return al unei metode. Indică faptul că metoda returnează un obiect de același tip ca obiectul însuși (în timp ce self indică faptul că returnează clasa în care este definită metoda). Ceea ce se potrivește excelent pentru descrierea interfețelor fluente:

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

Acest tip nu există în PHP 8 și nici nu va fi introdus în viitor. Resursele sunt o relicvă istorică din vremurile în care PHP nu avea încă obiecte. Treptat, resursele vor fi înlocuite cu obiecte și, în timp, acest tip va dispărea complet. De exemplu, PHP 8.0 înlocuiește resursa care reprezintă o imagine cu obiectul GdImage și resursa conexiunii curl cu obiectul CurlHandle.

Stringable

Este o interfață pe care o implementează automat fiecare obiect cu metoda magică __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

În definiția clasei, este posibil să se specifice explicit class Email implements Stringable, dar nu este necesar.

Acest stil de denumire este reflectat și de Nette\Utils\Html, care implementează interfața Nette\HtmlStringable în locul celei anterioare IHtmlString. Obiectele de acest tip, de exemplu, nu sunt escapate de Latte.

Varianța tipului, contravarianța, covarianța

Principiul de substituție Liskov (Liskov Substitution Principle – LSP) spune că descendenții unei clase și implementările unei interfețe nu trebuie să necesite niciodată mai mult și să furnizeze mai puțin decât părintele. Adică, metoda unui descendent nu trebuie să necesite mai multe argumente sau să accepte la parametri un interval mai restrâns de tipuri decât strămoșul și, invers, nu trebuie să returneze un interval mai larg de tipuri. Dar poate returna un interval mai restrâns. De ce? Deoarece altfel moștenirea nu ar funcționa deloc. Funcția ar accepta un obiect de un anumit tip, dar nu ar ști ce parametri pot fi transmiși metodelor și ce vor returna efectiv, deoarece orice descendent ar putea încălca acest lucru.

Deci, în OOP este valabil faptul că un descendent poate:

  • în parametri să accepte un interval mai larg de tipuri (acest lucru se numește contravarianță)
  • să returneze un interval mai restrâns de tipuri (covarianță)
  • și proprietățile nu pot schimba tipul (sunt invariante)

PHP știe acest lucru de la versiunea 7.4 și toate tipurile nou introduse în PHP 8.0 suportă, de asemenea, contravarianța și covarianța.

În cazul mixed, descendentul poate restrânge valoarea returnată la orice tip, însă nu void, deoarece nu este vorba de un tip de valoare, ci de absența acesteia. Nici descendentul nu poate să nu specifice tipul, deoarece și acest lucru permite absența.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

De asemenea, tipurile union pot fi extinse în parametri și restrânse în valorile returnate:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

În plus, false poate fi extins în parametru la bool sau, invers, bool în valoarea returnată poate fi restrâns la false.

Toate încălcările împotriva covarianței/contravarianței duc în PHP 8.0 la o eroare fatală.

În următoarele părți vom arăta ce sunt atributele, ce funcții și clase noi au apărut în PHP și vom prezenta Just in Time Compiler.