PHP 8.0: Podatkovni tipi (2/4)

pred 4 leti od David Grudl  

Izšla je različica PHP 8.0. Tako je polna novosti, kot še nobena različica pred njo. Njihova predstavitev je zahtevala kar štiri ločene članke. V tem drugem si bomo pogledali podatkovne tipe.

Vrnimo se v zgodovino. Bistven preboj PHP 7 je bila uvedba skalarnih type hintov. Skoraj se to ni zgodilo. Avtorico izjemne rešitve Andreo Faulds, ki je bila zahvaljujoč declare(strict_types=1) popolnoma povratno združljiva in izbirna, je skupnost grdo zavrnila. Na srečo sta se nje in njenega predloga takrat zavzela Anthony Ferrara, sprožila kampanjo in RFC je zelo tesno šel skozi. Ufff. Večino novosti v PHP 8 ima na vesti legendarni Nikita Popov in pri glasovanju so mu šle skozi kot po maslu. Svet se spreminja na bolje.

PHP 8 pripelje tipe do popolnosti. Izginila bo velika večina phpDoc anotacij @param, @return in @var v kodi in jih bodo nadomestili nativni zapis in predvsem kontrola s strani PHP motorja. V komentarjih bodo ostali le opisi struktur, kot je string[], ali bolj zapletene anotacije za PHPStan.

Union Types

Union tipi so naštevanje dveh ali več tipov, ki jih lahko vrednost prevzame:

class Button
{
	private string|object $caption;

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

Nekatere posebne union tipe je PHP poznal že prej. Na primer nullable tipe kot ?string, kar je ekvivalent union tipa string|null in vprašajni zapis lahko štejemo le za okrajšavo. Seveda deluje tudi v PHP 8, vendar ga ni mogoče kombinirati z navpičnicami, torej namesto ?string|object je treba pisati polno string|object|null. Nadalje je iterable bil vedno ekvivalent array|Traversable. Morda vas bo presenetilo, da je union tip pravzaprav tudi float, ki dejansko sprejema int|float, vendar pretipizira na float.

V unionih ni mogoče uporabljati pseudotipov void in mixed, ker to ne bi imelo nobenega smisla.

Nette je pripravljen za tipe zvez. V shemi Expect::from() jih sprejema, prav tako jih sprejemajo predstavniki. Uporabljate jih lahko na primer v metodah upodabljanja in akcije:

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

Nasprotno pa autowiring v Nette DI union tipe zavrača. Manjka zaenkrat use case, kjer bi bilo smiselno, da bi na primer konstruktor sprejemal bodisi ta ali oni objekt. Seveda, ko se pojavi, bo mogoče ustrezno prilagoditi obnašanje vsebnika.

Metode getParameterType(), getReturnType() in getPropertyType() v Nette\Utils\Reflection vržejo v primeru union tipa izjemo (v različici 3.1, v starejši 3.0 vračajo zaradi združljivosti null).

mixed

Pseudotip mixed pravi, da je vrednost lahko popolnoma karkoli.

V primeru parametrov in lastnosti gre pravzaprav za enako obnašanje, kot če nobenega tipa ne navedemo. Za kaj je torej dober? Da bi se dalo razlikovati, kdaj tip preprosto manjka in kdaj je resnično mixed.

V primeru povratne vrednosti funkcije in metode se nenavedba tipa od navedbe tipa mixed razlikuje. Gre pravzaprav za nasprotje void, saj pravi, da mora funkcija nekaj vrniti. Manjkajoči return je potem fatalna napaka.

V praksi bi ga morali uživati redko, saj lahko zahvaljujoč union tipom vrednost natančneje specificirate. Primeren je torej v izjemnih situacijah:

function dump(mixed $var): mixed
{
	// izpiši spremenljivko
	return $var;
}

false

Novi pseudotip false lahko nasprotno uporabljamo le v union tipih. Nastala je iz potrebe po nativnem opisu tipa povratne vrednosti pri nativnih funkcijah, ki zgodovinsko v primeru neuspeha vračajo false:

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

Zato ne obstaja tip true, ni mogoče uporabljati niti samega false ali false|null ali bool|false.

static

Pseudotip static lahko uporabimo le kot povratni tip metode. Pravi, da metoda vrača objekt istega tipa, kot je objekt sam (medtem ko self pravi, da vrača razred, v katerem je metoda definirana). Kar se odlično obnese za opis 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

Ta tip v PHP 8 ne obstaja in tudi v prihodnosti ne bo uveden. Resources so zgodovinski ostanek iz časov, ko PHP še ni imel objektov. Postopoma se bodo resources nadomeščali z objekti in sčasoma bo ta tip popolnoma izginil. Na primer PHP 8.0 nadomešča resource, ki predstavlja sliko, z objektom GdImage in resource povezave curl z objektom CurlHandle.

Stringable

Gre za vmesnik, ki ga samodejno implementira vsak objekt z magično metodo __toString().

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

function print(Stringable|string $s)
{
}

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

V definiciji razreda je mogoče eksplicitno navesti class Email implements Stringable, vendar to ni potrebno.

Ta slog poimenovanja odraža tudi Nette\Utils\Html, ki implementira vmesnik Nette\HtmlStringable namesto prejšnjega IHtmlString. Objekte tega tipa potem npr. Latte ne escapuje.

Type variance, contravariance, covariance

Liskovin princip zamenljivosti (Liskov Substitution Principle – LSP) pravi, da potomci razreda in implementacije vmesnika nikoli ne smejo zahtevati več in zagotavljati manj kot starš. Torej, da metoda potomca ne sme zahtevati več argumentov ali sprejemati pri parametrih ožjega razpona tipov kot prednik in nasprotno ne sme vračati širšega razpona tipov. Lahko pa vrača ožjega. Zakaj? Ker sicer dedovanje sploh ne bi delovalo. Funkcija bi sicer sprejela objekt določenega tipa, vendar ne bi vedela, katere parametre lahko metodam posreduje in kaj bodo dejansko vračale, saj bi katerikoli potomec to lahko porušil.

Torej v OOP velja, da potomec lahko:

  • v parametrih sprejema širši razpon tipov (temu se reče kontravarianca)
  • vrača ožji razpon tipov (kovarianca)
  • in lastnosti ne morejo spreminjati tipa (so invariantne)

PHP to zna od različice 7.4 in vsi novo uvedeni tipi v PHP 8.0 prav tako podpirajo kontravarianco in kovarianco.

V primeru mixed lahko potomec zoži povratno vrednost na katerikoli tip, nikakor pa ne void, ker ne gre za tip vrednosti, ampak za njeno odsotnost. Niti potomec ne more ne navesti tipa, ker to prav tako dopušča odsotnost.

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

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

Tudi union tipe je mogoče v parametrih razširjati in v povratnih vrednostih ožiti:

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

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

Nadalje false lahko v parametru razširimo na bool ali nasprotno bool v povratni vrednosti zožimo na false.

Vse kršitve kovariance/kontravariance vodijo v PHP 8.0 do fatalne napake.

V prihodnjih delih si bomo pokazali, kaj so atributi, katere nove funkcije in razredi so se pojavili v PHP in predstavili si bomo Just in Time Compiler.