PHP 8.0: Popoln pregled novosti (1/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 prvem si bomo pogledali, kaj novega prinaša po jezikovni plati.

Še preden se poglobimo v PHP, vedite, da je trenutna različica Nette popolnoma pripravljena za osmico. Še več, kot darilo je izšel celo Nette 2.4, ki je z njo popolnoma združljiv, tako da vam z vidika ogrodja nič ne preprečuje, da začnete uporabljati novo različico.

Poimenovani argumenti

In začnimo kar z bombo, ki jo lahko mirno označimo za game changer. Po novem lahko funkcijam in metodam argumente posredujemo ne le pozicijsko, ampak tudi po imenih. Kar je popolnoma odlično v primeru, da ima metoda res veliko parametrov:

class Response implements IResponse
{
	public function setCookie(
		string $name,
		string $value,
		string|DateInterface|null $time,
		string $path = null,
		string $domain = null,
		bool $secure = null,
		bool $httpOnly = null,
		string $sameSite = null
	) {
		...
	}
}

Prva dva argumenta posredujemo pozicijsko, ostale po imenu: (poimenovani morajo vedno slediti pozicijskim)

$response->setCookie('lang', $lang, sameSite: 'None');

// namesto norega
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Pred prihodom te funkcije je bilo v načrtu ustvariti v Nette nov API za pošiljanje piškotkov, ker je število parametrov setCookie() res naraslo in pozicijski zapis je bil nepregleden. Zdaj to ni več potrebno, ker so poimenovani argumenti v tem primeru najbolj praktičen API. IDE jih bo predlagal in imajo tipsko kontrolo.

Odlično se obnesejo tudi za dodatno pojasnitev logičnih parametrov, kjer njihova uporaba sicer ni nujna, a samo true ali false ne pove veliko:

// prej
$db = $container->getService(Database::class, true);

// zdaj
$db = $container->getService(Database::class, need: true);

Imena parametrov zdaj postajajo del javnega API-ja. Ni jih mogoče poljubno spreminjati kot doslej. Zato tudi Nette prehaja skozi revizijo, ali imajo vsi parametri ustrezno poimenovanje.

Poimenovane argumente lahko uporabljamo tudi v kombinaciji z variadics:

function variadics($a, ...$args) {
	dump($args);
}

variadics(a: 1, b: 2, c: 3);
// v $args bo ['b' => 2, 'c' => 3]

Po novem lahko polje $args vsebuje tudi neštevilske ključe, kar je določen BC break. Enako velja tudi za obnašanje funkcije call_user_func_array($func, $args), kjer zdaj ključi v polju $args igrajo pomembno vlogo. Nasprotno pa so funkcije iz družine func_*() od poimenovanih argumentov zaščitene.

S poimenovanimi argumenti je tesno povezano tudi dejstvo, da splat operator ... lahko zdaj razširi tudi asociativna polja:

variadics(...['b' => 2, 'c' => 3]);

Presenetljivo to zaenkrat ne deluje znotraj polj:

$arr = [ ...['a' => 1, 'b' => 2] ];
// Fatal error: Cannot unpack array with string keys

Kombinacija poimenovanih argumentov in variadics daje možnost končno imeti fiksno sintakso na primer za metodo presenterja link(), kateri zdaj lahko poimenovane argumente posredujemo enako kot pozicijske:

// prej
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // moralo je biti polje

// zdaj
$presenter->link('Product:detail', $id, page: 1);

Sintaksa za poimenovane argumente je veliko bolj seksi kot pisanje polj, zato jo je “Latte takoj posvojil:https://blog.nette.org/…ot-for-least#…”, kjer jo lahko uporabljamo na primer v oznakah {include} in {link}:

{include 'file.latte' arg1: 1, arg2: 2}
{link default page: 1}

K poimenovanim parametrom se bomo vrnili še v tretjem delu v povezavi z atributi.

Izraz lahko vrže izjemo

Vržanje izjeme je zdaj izraz. Lahko ga na primer ovijete z oklepaji in dodate v pogoj if. Hmmm, to ne zveni preveč praktično. Ampak tole je že bolj zanimivo:

// prej
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('vrednost ni nastavljena');
}
$value = $arr['value'];


// zdaj, ko je throw izraz
$value = $arr['value'] ?? throw new \InvalidArgumentException('vrednost ni nastavljena');

Ker puščične funkcije lahko doslej vsebujejo le en izraz, lahko zahvaljujoč tej funkciji mečejo izjeme:

// samo en izraz
$fn = fn() => throw new \Exception('ups');

Match Expressions

Konstrukcija switch-case ima dve veliki slabosti:

  • uporablja nestrogo primerjanje == namesto ===
  • morate paziti, da pomotoma ne pozabite na break

PHP zato prihaja z alternativo v obliki nove konstrukcije match, ki uporablja strogo primerjanje in nasprotno ne uporablja break.

Primer kode switch:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('ni najdeno');
        break;
    case 500:
        $message = $this->formatMessage('napaka strežnika');
        break;
    default:
        $message = 'neznana koda stanja';
        break;
}

In enako (le s strogim primerjanjem) zapisano s pomočjo match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('ni najdeno'),
    500 => $this->formatMessage('napaka strežnika'),
    default => 'neznana koda stanja',
};

Opazite, da match ni kontrolna struktura kot switch, ampak izraz. Njegovo končno vrednost v primeru dodelimo spremenljivki. Hkrati so tudi posamezne “možnosti” izrazi, zato ni mogoče zapisati več korakov, kot v primeru switch.

Če ne pride do ujemanja z nobeno od izbir (in ne obstaja klavzula default), se vrže izjema UnhandledMatchError.

Mimogrede, v Latte obstajajo tudi značke {switch}, {case} in {default}. Njihovo delovanje natančno ustreza novemu match. Uporabljajo strogo primerjanje, ne zahtevajo break in v case je mogoče navesti več vrednosti, ločenih z vejicami.

Nullsafe operator

Opcijsko veriženje (optional chaining) omogoča pisanje izraza, katerega vrednotenje se ustavi, če naleti na null. In to zahvaljujoč novemu operatorju ?->. Nadomesti veliko kode, ki bi sicer ponavljajoče preverjala null:

$user?->getAddress()?->street

// pomeni približno
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Zakaj “pomeni približno”? Ker se v resnici izraz vrednoti bolj domiselno in se noben korak ne ponovi. Na primer $user->getAddress() se kliče le enkrat, torej ne more nastati problem, povzročen s tem, da bi metoda prvič in drugič vrnila nekaj drugega.

To svežo izboljšavo je pred enim letom prinesel Latte. Zdaj prihaja v sam PHP. Odlično.

Constructor property promotion

Sintaktični sladkor, ki prihrani dvojno pisanje tipa in štirikratno pisanje spremenljivke. Škoda le, da ni prišel v času, ko nismo imeli tako pametnih IDE, ki to danes pišejo namesto nas 🙂

class Facade
{
	private Nette\Database\Connection $db;
	private Nette\Mail\Mailer $mailer;

	public function __construct(Nette\Database\Connection $db, Nette\Mail\Mailer $mailer)
	{
		$this->db = $db;
		$this->mailer = $mailer;
	}
}
class Facade
{
	public function __construct(
		private Nette\Database\Connection $db,
		private Nette\Mail\Mailer $mailer,
	) {}
}

Z Nette DI deluje, lahko takoj začnete uporabljati.

Strogo obnašanje aritmetičnih in bitnih operatorjev

Kar je nekoč dinamične skriptne jezike izstrelilo na vrh, je sčasoma postalo njihova najšibkejša točka. PHP se je nekoč znebil “magic quotes”, registriranja globalnih spremenljivk in zdaj sproščeno obnašanje zamenjuje strogost. Čas, ko ste v PHP lahko seštevali, množili itd. skoraj katerekoli podatkovne tipe, pri katerih to ni imelo prav nobenega smisla, je že davno minil. Od različice 7.0 je PHP vedno bolj strog in od različice 8.0 že poskus uporabe kateregakoli aritmetičnega/bitnega operatorja pri poljih, objektih ali virih konča s TypeError. Izjema je seštevanje polj.

// aritmetični in bitni operatorji
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Razumnejše primerjanje nizov in števil

Ali make loose operator great again.

Zdelo bi se, da za loose operator == ni več prostora, da gre le za tipkarsko napako pri pisanju ===, ampak ta sprememba ga vrača nazaj na zemljevid. Ko ga že imamo, naj se obnaša razumno. Posledica prejšnjega “nerazumnega” primerjanja je bilo na primer obnašanje in_array(), ki vas je lahko neprijetno presenetilo:

$validValues = ['foo', 'bar', 'baz'];
$value = 0;

dump(in_array($value, $validValues));
// presenetljivo je vračalo true
// od PHP 8.0 vrača false

Sprememba v obnašanju == se nanaša na primerjanje števil in “številskih” nizov in jo prikazuje naslednja tabela:

Primerjava Pred PHP 8.0
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42 " true true
42 == "42foo" true false
42 == "abc42" false false
"42" == " 42" true true
"42" == "42 " false true

Presenetljivo je, da gre za BC break v samem temelju jezika, ki je bil odobren brez kakršnegakoli odpora. In to je dobro. Tu bi JavaScript lahko zelo zavidal.

Poročanje napak

Mnoge interne funkcije zdaj sprožijo TypeError in ValueError namesto opozoril, ki jih je bilo mogoče zlahka spregledati. Reklasificirana je bila vrsta opozoril jedra. Shutup operator @ zdaj ne utiša fatalnih napak. In PDO v privzetem načinu meče izjeme.

Te stvari je Nette vedno poskušal nekako rešiti. Tracy je prilagajal obnašanje shutup operatorja, Database je preklapljal obnašanje PDO, Utils vsebuje nadomestke standardnih funkcij, ki mečejo izjeme namesto neopaznih opozoril itd. Lepo je videti, da stroga smer, ki jo ima Nette v svoji DNK, postaja nativna smer jezika.

Polja z negativnim indeksom

$arr[-5] = 'prvi';
$arr[] = 'drugi';

Kakšen bo ključ drugega elementa? Prej je bil 0, od PHP 8 je -4.

Končna vejica

Zadnje mesto, kjer ni mogla biti končna vejica, je bila definicija parametrov funkcije. To je zdaj preteklost:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // končna vejica
	) {
		....
	}

$object::class

Magična konstanta ::class deluje tudi z objekti $object::class, s čimer popolnoma nadomešča funkcijo get_class().

catch brez spremenljivke

In končno: v klavzuli catch ni potrebno navajati spremenljivke za izjemo:

try {
	$container->getService(Database::class);

} catch (MissingServiceException) {  // brez $e
	$logger->log('....');
}

V prihodnjih delih nas čakajo bistvene novosti v podatkovnih tipih, pokazali si bomo, kaj so atributi, katere nove funkcije in razredi so se pojavili v PHP in predstavili si bomo Just in Time Compiler.