PHP 8.0: Adattípusok (2/4)

4 éve írta David Grudl  

Megjelent a PHP 8.0-s verziója. Annyira tele van újdonságokkal, mint még egyetlen verzió sem korábban. Bemutatásukhoz egyenesen négy különálló cikkre volt szükség. Ebben a másodikban az adattípusokat nézzük meg.

Térjünk vissza a történelembe. A PHP 7 alapvető áttörése a skaláris type hintek bevezetése volt. Majdnem nem történt meg. A csodálatos megoldás szerzőjét, Andrea Faulds, amely a declare(strict_types=1)-nek köszönhetően teljesen visszamenőlegesen kompatibilis és opcionális volt, a közösség csúnyán elutasította. Szerencsére akkoriban Anthony Ferrara mellé állt, kampányt indított, és az RFC nagyon szűken átment. Húha. A PHP 8 legtöbb újdonsága a legendás Nikita Popov nevéhez fűződik, és a szavazáson simán átmentek neki. A világ jobbá válik.

A PHP 8 tökéletesíti a típusokat. A kód phpDoc annotációinak (@param, @return és @var) túlnyomó többsége eltűnik, és helyüket natív írásmód és főleg a PHP motor általi ellenőrzés veszi át. A kommentekben csak a struktúrák leírásai maradnak, mint a string[] vagy bonyolultabb annotációk a PHPStan számára.

Union típusok

Az union típusok két vagy több olyan típus felsorolása, amelyeket egy érték felvehet:

class Button
{
	private string|object $caption;

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

Néhány speciális union típust a PHP már korábban is ismert. Például a nullable típusokat, mint a ?string, ami a string|null union típus ekvivalense, és a kérdőjeles írásmódot csak rövidítésnek tekinthetjük. Természetesen ez PHP 8-ban is működik, de nem kombinálható függőleges vonalakkal, tehát a ?string|object helyett a teljes string|object|null-t kell írni. Továbbá az iterable mindig is az array|Traversable ekvivalense volt. Talán meglepő, hogy az union típus tulajdonképpen a float is, amely valójában int|float-ot fogad el, de float-ra konvertálja.

Az unionokban nem használhatók a void és mixed pszeudotípusok, mert ennek semmi értelme nem lenne.

A Nette készen áll az union típusokra. A Schemában a Expect::from() elfogadja őket, és a prezenterek is elfogadják őket. Használhatod őket például a render és action metódusokban:

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

Ezzel szemben a Nette DI autowiring elutasítja az union típusokat. Hiányzik még a use case, ahol értelme lenne annak, hogy például a konstruktor vagy ezt, vagy azt az objektumot fogadja el. Természetesen, ha megjelenik, a konténer viselkedését ennek megfelelően lehet majd módosítani.

A getParameterType(), getReturnType() és getPropertyType() metódusok a Nette\Utils\Reflection-ben kivételt dobnak union típus esetén (a 3.1-es verzióban, a régebbi 3.0-ban a kompatibilitás miatt null-t adnak vissza).

mixed

A mixed pszeudotípus azt jelenti, hogy az érték bármi lehet.

Paraméterek és property-k esetén ez tulajdonképpen ugyanaz a viselkedés, mintha nem adnánk meg típust. Mire jó tehát? Hogy meg lehessen különböztetni, mikor hiányzik egyszerűen a típus, és mikor valóban mixed.

Függvények és metódusok visszatérési értékénél a típus meg nem adása eltér a mixed típus megadásától. Ez tulajdonképpen a void ellentéte, mivel azt mondja, hogy a függvénynek valamit vissza kell adnia. A hiányzó return ekkor fatális hiba.

A gyakorlatban ritkán kellene használni, mert az union típusoknak köszönhetően pontosabban meg lehet határozni az értéket. Tehát kivételes helyzetekben hasznos:

function dump(mixed $var): mixed
{
	// változó kiírása
	return $var;
}

false

Az új false pszeudotípust ezzel szemben csak union típusokban lehet használni. Azért jött létre, hogy natívan le lehessen írni a natív függvények visszatérési értékének típusát, amelyek történelmileg sikertelenség esetén false-t adnak vissza:

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

Ebből az okból nem létezik true típus, nem használható önmagában a false, sem a false|null, sem a bool|false.

static

A static pszeudotípust csak metódus visszatérési típusaként lehet használni. Azt mondja, hogy a metódus ugyanolyan típusú objektumot ad vissza, mint maga az objektum (míg a self azt mondja, hogy azt az osztályt adja vissza, amelyben a metódus definiálva van). Ami kiválóan alkalmas a fluent interface-ek leírására:

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

Ez a típus PHP 8-ban nem létezik, és a jövőben sem kerül bevezetésre. Az erőforrások (resources) történelmi maradványok abból az időből, amikor a PHP-nak még nem voltak objektumai. Fokozatosan az erőforrásokat objektumokra cserélik, és idővel ez a típus teljesen eltűnik. Például a PHP 8.0 a képet reprezentáló erőforrást a GdImage objektumra, a curl kapcsolat erőforrását pedig a CurlHandle objektumra cseréli.

Stringable

Ez egy interfész, amelyet automatikusan implementál minden objektum a mágikus __toString() metódussal.

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

function print(Stringable|string $s)
{
}

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

Az osztálydefinícióban explicit módon meg lehet adni a class Email implements Stringable-t, de ez nem szükséges.

Ez az elnevezési stílus tükröződik a Nette\Utils\Html-ben is, amely a Nette\HtmlStringable interfészt implementálja a korábbi IHtmlString helyett. Az ilyen típusú objektumokat például a Latte nem escapeli.

Típusvariancia, kontravariancia, kovariancia

A Liskov-féle helyettesítési elv (Liskov Substitution Principle – LSP) kimondja, hogy az osztály leszármazottai és az interfész implementációi soha nem követelhetnek többet és nem nyújthatnak kevesebbet, mint a szülő. Tehát a leszármazott metódusa nem követelhet több argumentumot, vagy nem fogadhat el szűkebb típus tartományt a paramétereknél, mint az ős, és fordítva, nem adhat vissza szélesebb típus tartományt. De adhat vissza szűkebbet. Miért? Mert különben az öröklődés egyáltalán nem működne. A függvény ugyan elfogadna egy bizonyos típusú objektumot, de nem tudná, milyen paramétereket lehet átadni a metódusoknak, és mit fognak valójában visszaadni, mert bármelyik leszármazott ezt felboríthatná.

Tehát az OOP-ban érvényes, hogy a leszármazott:

  • a paraméterekben szélesebb típus tartományt fogadhat el (ezt nevezik kontravarianciának)
  • szűkebb típus tartományt adhat vissza (kovariancia)
  • és a property-k nem változtathatják meg a típust (invariánsak)

A PHP ezt a 7.4-es verziótól tudja, és a PHP 8.0-ban bevezetett összes új típus is támogatja a kontravarianciát és a kovarianciát.

A mixed esetén a leszármazott szűkítheti a visszatérési értéket bármilyen típusra, de nem void-ra, mert az nem az érték típusa, hanem annak hiánya. A leszármazott sem hagyhatja el a típust, mert az is megengedi a hiányt.

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

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

Az union típusokat is lehet bővíteni a paraméterekben és szűkíteni a visszatérési értékekben:

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

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

Továbbá a false a paraméterben bővíthető bool-ra, vagy fordítva a bool a visszatérési értékben szűkíthető false-ra.

Minden kovariancia/kontravariancia elleni vétség PHP 8.0-ban fatális hibához vezet.

A következő részekben megmutatjuk, mik azok az attribútumok, milyen új függvények és osztályok jelentek meg a PHP-ban, és bemutatjuk a Just in Time Compilert.