PHP 8.0: popoln pregled novic (1/4)

pred 4 leti od David Grudl  

Prav zdaj je izšla različica PHP 8.0. Polna je novosti, kot še nobena različica do zdaj. Njihova predstavitev si je zaslužila štiri ločene članke. V prvem si bomo ogledali, kaj prinaša na ravni jezika.

Preden se poglobimo v PHP, naj povemo, da je trenutna različica Nette v celoti pripravljena na osmo različico. Poleg tega je bila kot darilo izdana popolnoma združljiva različica Nette 2.4, tako da z vidika ogrodja ni ničesar, kar bi vas oviralo pri uporabi.

Poimenovani argumenti

Takoj začnimo z bombo, ki bi jo lahko pogumno označili kot spreminjajočo igro. Argumentov lahko zdaj funkcijam in metodam ne posredujemo le pozicijsko, temveč tudi glede na njihovo ime. Kar je povsem kul, če ima metoda res preveč 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 se posredujeta pozicijsko, drugi pa po imenu: (poimenovani morajo slediti za pozicijskimi)

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

// instead of the horrible
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Pred uvedbo te funkcije je bilo načrtovano, da se ustvari nov API za pošiljanje piškotkov v Nette, saj je število argumentov za setCookie() res naraslo, položajni zapis pa je bil zmeden. To ni več potrebno, saj so poimenovani argumenti v tem primeru najprimernejši API. IDE jih bo namignil in zagotovljena je tipska varnost.

Idealno so primerni tudi za razlago logičnih argumentov, kjer njihova uporaba ni nujna, vendar navaden true ali false ne zadostuje:

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

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

Imena argumentov so zdaj del javnega vmesnika API. Ni jih več mogoče poljubno spreminjati. Zaradi tega celo Nette izvaja revizijo, s katero ugotavlja, ali imajo vsi argumenti ustrezno ime.

Poimenovani argumenti se lahko uporabljajo tudi v kombinaciji z variadikami:

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

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

Polje $args lahko zdaj vsebuje tudi neštevilčne ključe, kar je nekakšen prelom BC. Enako velja za obnašanje call_user_func_array($func, $args), kjer ključi v polju $args zdaj igrajo veliko pomembnejšo vlogo. Nasprotno pa so funkcije družine func_*() zaščitene pred poimenovanimi argumenti.

Poimenovani argumenti so tesno povezani z dejstvom, da lahko operator splat ... zdaj razširi asociativna polja:

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

Presenetljivo je, da trenutno ne deluje znotraj polj:

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

Kombinacija poimenovanih argumentov in variadike omogoča, da imamo končno fiksno sintakso, na primer za metodo link(), ki ji lahko posredujemo tako poimenovane kot pozicijske argumente:

// before
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // had to be an array

// now
$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 argumentom se bomo vrnili v tretjem delu serije v povezavi z atributi.

Izraz lahko vrže izjemo

Odvrženje izjeme je zdaj izraz. Lahko ga zavijete v oklepaje in dodate v pogoj if. Hmmm, to se ne sliši preveč praktično. Vendar je to veliko bolj zanimivo:

// before
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('value not set');
}
$value = $arr['value'];


// now, when throw is an expression
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Ker so lahko puščične funkcije do zdaj vsebovale le en izraz, lahko zdaj zaradi te lastnosti mečejo izjeme:

// only single expression
$fn = fn() => throw new \Exception('oops');

Izraz ujemanja

Izjava switch-case ima dve glavni pomanjkljivosti:

  • uporablja strogo primerjavo == namesto ===
  • paziti morate, da ne bi pomotoma pozabili na break

Zaradi tega ima PHP alternativo v obliki novega izraza match, ki uporablja strogo primerjavo in, nasprotno, ne uporablja break.

switch primer kode:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('not found');
        break;
    case 500:
        $message = $this->formatMessage('server error');
        break;
    default:
        $message = 'unknown status code';
        break;
}

In enako (le s strogo primerjavo), napisano z uporabo match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('not found'),
    500 => $this->formatMessage('server error'),
    default => 'unknown status code',
};

Upoštevajte, da match ni kontrolna struktura kot switch, temveč izraz. V primeru njegovo dobljeno vrednost pripišemo spremenljivki. Hkrati so tudi posamezne “možnosti” izrazi, zato ni mogoče zapisati več korakov, kot v primeru switch.

V primeru, da ni ujemanja (in ni privzetega stavka), se vrže izjema UnhandledMatchError.

Mimogrede, v programu Latte obstajajo tudi oznake {switch}, {case} in {default}. Njihova funkcija natančno ustreza novi oznaki match. Uporabljajo strogo primerjavo, ne zahtevajo break in v case je mogoče navesti več vrednosti, ločenih z vejicami.

Operator Nullsafe

Neobvezno veriženje omogoča zapis izraza, katerega vrednotenje se ustavi, če naleti na ničlo. To omogoča novi operator ?->. Z njim nadomestimo veliko kode, ki bi sicer morala večkrat preveriti ničlo:

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

// approximately translates to
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Zakaj “približno”? Ker se v resnici izraz ovrednoti bolj domiselno, tako da se noben korak ne ponovi. Na primer, $user->getAddress() se kliče samo enkrat, zato ne moremo naleteti na težavo, ki bi jo povzročila metoda, ki bi prvič in drugič vrnila nekaj drugega.

To funkcijo je pred letom dni:https://blog.nette.org/…om-functions prinesel Latte. Zdaj jo je prevzel tudi PHP. Odlično.

Spodbujanje lastnosti konstruktorja

Sintaktični sladkor, ki prihrani dvakratno zapisovanje tipa in štirikratno zapisovanje spremenljivke. Škoda, da se ni pojavil v času, ko nismo imeli tako pametnih IDE, ki ga 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,
	) {}
}

Deluje z Nette DI, lahko ga začnete uporabljati.

Strožji pregledi tipov za aritmetične in bitne operatorje

Kar je nekoč pomagalo dinamičnim skriptnim jezikom, da so postali prepoznavni, je postalo njihova najšibkejša točka. Nekoč se je PHP znebil “čarobnih narekovajev”, registracije globalnih spremenljivk, zdaj pa sproščeno obnašanje nadomešča strogost. Časi, ko ste lahko v PHP dodajali, množili itd. skoraj vse vrste podatkov, za katere to ni imelo smisla, so že zdavnaj minili. Od različice 7.0 je PHP vse strožji, od različice 8.0 pa se poskus uporabe kakršnih koli aritmetičnih/bitnih operatorjev na poljih, predmetih ali virih konča z TypeError. Izjema je dodajanje polj.

// arithmetic and bitwise operators
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Varnejše primerjave nizov s številkami

Ali pa ponovno naredite odličen operater loose.

Zdi se, da za operator loose == ni več prostora, da gre le za tiskarsko napako pri pisanju ===, vendar ga ta sprememba spet vrača na zemljevid. Če ga že imamo, naj se obnaša razumno. Zaradi prejšnje “nerazumne” primerjave bi lahko na primer in_array() neprijetno trotil:

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

dump(in_array($value, $validValues));
// surprisingly returned true
// since PHP 8.0 returns false

Sprememba obnašanja == se nanaša na primerjavo števil in “številskih” nizov in je prikazana v naslednji tabeli:

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 je bil ta prelom BC v samem jedru jezika odobren brez kakršnega koli odpora. In to je dobro. JavaScript bi bil lahko v tem pogledu zelo ljubosumen.

Poročanje o napakah

Številne notranje funkcije zdaj namesto opozoril, ki jih je bilo preprosto prezreti, sprožijo TypeError in ValueError. Številna opozorila jedra so bila prerazvrščena. Operator za izklop @ zdaj ne utiša usodnih napak. In PDO privzeto meče izjeme.

Nette je te stvari vedno poskušal rešiti na nek način. Tracy je spremenil obnašanje operatorja shutup, baza podatkov je spremenila obnašanje PDO, Utils vsebuje nadomestne standardne funkcije, ki namesto lahko spregledljivih opozoril mečejo izjeme, itd. Lepo je videti, da stroga smer, ki jo ima Nette v svoji DNK, postane izvorna smer jezika.

Negativni prirastki ključa polja

$arr[-5] = 'first';
$arr[] = 'second';

Kakšen bo ključ drugega elementa? Včasih je bil 0, since PHP 8 it’s -4.

Končna vejica

Zadnje mesto, kjer vejice ni bilo mogoče postaviti, je bila definicija argumentov funkcij. To je stvar preteklosti:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // trailing comma
	) {
		....
	}

$object::class

Čarobna konstanta ::class deluje tudi pri predmetih $object::class in popolnoma nadomesti funkcijo get_class().

Ujemanje izjem samo po vrsti

In končno: v stavku catch ni treba določiti spremenljivke za izjemo:

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

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

V naslednjih delih si bomo ogledali glavne novosti v zvezi s podatkovnimi tipi, pokazali bomo, kaj so atributi, katere nove funkcije in razredi so se pojavili v PHP in predstavili prevajalnik Just in Time.