PHP 8.0: Co je nového? (1/4)

před měsícem od David Grudl     edit

Vyšlo PHP verze 8.0. Je tak nadupané novinkami, jako nebyla žádná verze předtím. Jejich představení si vyžádalo rovnou čtyři samostatné články. V tomhle prvním se podíváme, co nového přináší po jazykové stránce.

Ještě než se ponoříme do PHP, vězte, že aktuální verze Nette je pro osmičku plně připravena. Ba co víc, jako dárek dokonce vyšlo i Nette 2.4, které je s ní plně kompatibilní, takže z pohledu frameworku vám nic nebrání novou verzi začít používat.

Pojmenované argumenty

A začneme rovnou bombou, kterou lze směle označit za game changer. Nově lze předávat funkcím a metodám argumenty nejen pozičně, ale i podle jmen. Což je naprosto skvělé v případě, že metoda má parametrů opravdu hodně:

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
	) {
		...
	}
}

První dva argumenty předáme pozičně, další podle jména: (pojmenované musí vždy následovat až po pozičních)

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

// místo šíleného
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Před příchodem této featury bylo v plánu vytvořit v Nette nové API pro odesílání cookies, protože počet parametrů setCookie() opravdu narostl a poziční zápis byl nepřehledný. Teď už to není potřeba, protože pojmenované argumenty jsou v tomhle případě tím nejpraktičtějším API. IDE je budou napovídat a mají typovou kontrolu.

Skvěle se hodí i pro dovysvětlení logických parametrů, kde jejich užití sice není nutné, ale samotné true nebo false mnoho neříká:

// dříve
$db = $container->getService(Database::class, true);

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

Názvy parametrů se nyní stávají součástí veřejného API. Není možné je libovolně měnit jako doposud. Z toho důvodu i Nette prochází auditem, zda všechny parametry mají vhodné pojmenování.

Pojmenované argumenty lze používat také v kombinaci s variadics:

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

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

Nově tak může pole $args obsahovat i nenumerické klíče, což je určitý BC break. Totéž se týká i chování funkce call_user_func_array($func, $args), kde nyní hrají klíče v poli $args signifikantní roli. Naopak funkce z rodiny func_*() jsou od pojmenovaných argumentů odstíněny.

S pojmenovanými argumenty také úzce souvisí fakt, že splat operátor ... může nyní rozbalit i asociativní pole:

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

Kupodivu to zatím nefunguje uvnitř polí:

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

Kombinace pojmenovaných argumentů a variadics dává možnost konečně mít pevnou syntax třeba pro metodu presenteru link(), které teď můžeme pojmenované argumenty předávat stejně jako poziční:

// dříve
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // muselo být pole

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

Syntax pro pojmenované parametry je mnohem více sexy, než zápis polí, proto si ji také ihned osvojilo Latte, kde ji lze používat například ve značkách {include} a {link}:

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

K pojmenovaným parametrům se ještě vrátíme ve třetím díle v souvislosti s atributy.

Výraz může vyhodit výjimku

Vyhození výjimky je nyní výraz. Můžete jej třeba obalit závorkami a přidat do podmínky if. Hmmm, to nezní příliš prakticky. Ale tohle už je zajímavější:

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


// nyní, když je throw výraz
$value = $arr['value'] ?? throw new \InvalidArgumentException('value not set');

Protože arrow funkce mohou dosud obsahovat pouze jeden výraz, díky této feature mohou vyhazovat výjimky:

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

Match Expressions

Konstrukce switch-case má dvě velké nectnosti:

  • používá nestriktní porovnávání == místo ===
  • musíte dávat pozor, abyste omylem nezapomněli na break

PHP proto přichází s alternativou v podobě nové konstrukce match, která používá striktní porovnávání a naopak nepoužívá break.

Příklad kódu switch:

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;
}

A totéž (jen se striktním porovnáváním) zapsané pomocí match:

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

Všimněte si, že match není kontrolní struktura jakoswitch, ale výraz. Jeho výslednou hodnotu v příkladu přiřazujeme do proměnné. Zároveň i jednotlivé „možnosti“ jsou výrazy, nelze tedy zapsat více kroků, jako v případě switch.

Pokud nedojde ke shodě se žádnou z voleb (a neexistuje klauzule default), vyhodí se výjimka UnhandledMatchError.

Mimochodem, v Latte také existují značky {switch}, {case} a {default}. Jejich fungování odpovídá přesně novému match. Používají striktní porovnávání, nevyžadují break a v case je možné uvést více hodnot oddělených čárkami.

Nullsafe operator

Volitelné řetězení (optional chaining) umožňuje psát výraz, jehož vyhodnocování se zastaví, pokud narazí na null. A to díky novému operátoru ?->. Nahradí spoustu kódu, který by jinak opakovaně kontroloval null:

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

// znamená cca
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Proč „znamená cca“? Protože ve skutečnosti se výraz vyhodnocuje důmyslněji a žádný krok se neopakuje. Například $user->getAddress() se volá jen jednou, tedy nemůže nastat problém způsobený tím, že by metoda poprvé a podruhé vrátila něco jiného.

Tuhle svěží vychytávku přineslo před rokem Latte. Nyní se dostává do samotného PHP. Paráda.

Constructor property promotion

Syntaktický cukr, který ušetří dvojí psaní typu a čtyřnásobné psaní proměnné. Škoda jen, že nepřišel v době, kdy jsme neměli tak chytré IDE, které to dnes píší za nás 🙂

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,
	) {}
}

S Nette DI funguje, můžete hned začít používat.

Striktní chování aritmetických a bitových operátorů

Co kdysi vystřelilo dynamické skriptovací jazyky na výsluní, se časem stalo jejich nejslabším místem. PHP se kdysi zbavovalo „magic quotes“, registrování globálních proměnných a nyní uvolněné chování střídá striktnost. Doba, kdy jste v PHP mohli sčítat, násobit atd. téměř jakékoliv datové typy, u nichž to nedávalo pražádný žádný smysl, je dávno pryč. Počínaje verzí 7.0 je PHP čím dál tím striktnější a od verze 8.0 už pokus o použití jakéhokoliv aritmetického/bitového operátoru u polí, objektů či resources končí TypeError. Výjimkou je sčítání polí.

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

Rozumnější porovnávání řetězců a čísel

Aneb make loose operator great again.

Zdálo by se, že pro loose operátor == již není místo, že jde jen o překlep při psaní ===, ale tato změna jej vrací znovu na mapu. Když už ho máme, ať se chová rozumně. Důsledkem dřívějšího „nerozumného“ porovnávání bylo třeba chování in_array(), které vás mohlo nemile vypéct:

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

dump(in_array($value, $validValues));
// překvapivě vracelo true
// od PHP 8.0 vrací false

Změna v chování == se týká porovnávání čísel a „číselných“ řetězců a ukazuje ji následující tabulka:

Comparison Before 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

Překvapivé je, že jde o BC break v samotném základu jazyka, který byl schválen bez jakéhokoliv odporu. A to je dobře. Tady by JavaScript mohl hodně závidět.

Reportování chyb

Mnohé interní funkce nyní vyvolávají TypeError a ValueError místo varování, které se dalo snadno opomenout. Byla reklasifikována řada varování jádra. Shutup operátor @ nyní neztiší fatální chyby. A PDO v defaultním režimu vyhazuje výjimky.

Tyhle věci se vždy Nette snažilo nějakým způsobem řešit. Tracy upravovala chování shutup operátoru, Database přepínalo chování PDO, Utils obsahuje náhrady standardních funkcí, které vyhazují výjimky místo nenápadných varování atd. Je fajn vidět, že striktní směr, který má v Nette ve své DNA, se stává nativním směrem jazyka.

Pole s negativní indexem

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

Jaký bude klíč druhého prvku? Dříve to byla 0, od PHP 8 je to -4.

Koncová čárka

Poslední místo, kde nemohla být koncová čárka, byla definice parametrů funkce. To už je minulostí:

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

$object::class

Magická konstanta ::class funguje i s objekty $object::class, čímž zcela nahrazuje funkci get_class().

catch bez proměnné

A nakonec: v klauzuli catch není nutné uvádět proměnnou pro výjimku:

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

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

V příštích dílech nás čekají zásadní novinky v datových typech, ukážeme si co jsou to atributy, jaké se v PHP objevily nové funkce a třídy a představíme si Just in Time Compiler.

Komentáře (RSS)

  1. Díky, konečně se toho někdo ujal a pěkně vysvětlil co vše PHP 8 mění… Těším se na další článek!

    před měsícem
  2. Až do teď, jsem místo podmiňování, nebo switchování používal pole.
    Tvůj zápis v mém poli pro MATCH by u mě vypadal takto:

    $statusMessages = [
    200 ⇒ null,
    300 ⇒ null,
    400 ⇒ ‚not found‘,
    500 ⇒ ‚server error‘,
    };

    $message = $statusMessages[$statusCode] ?? ‚unknown status code‘;

    Match zjednodušuje tento zápis, protože nemusím používat ?? operátor, ani v poli definovat 2 klíče se stejnou hodnotou zvlášť.

    Otázka ale zní:
    Pole jsem si nadefinoval někde jako třídní proměnnou hodnot a poté jej používal na více místech.
    Lze match nadefinovat někde bokem jako například v C parametrizované makro, nebo je nutno na to vytvořit například funkci?

    před měsícem · replied [5] David Grudl [8] jokas
  3. Otazka k te kompatibilite… Tracy podporuje PHP8 ve verzi, ktera ma konflikt s DI<3.

    před měsícem · replied [5] David Grudl
  4. Díky za skvělý článek. Našel jsem jednu drobnou chybu: 42 == "42 " je true i v PHP 8: https://3v4l.org/qq6Yb

    před měsícem · replied [5] David Grudl
  5. #2 Polki ono to není z toho příkladu zřejmé, ale výhoda match je v tom, že pokud hodnotou bude nějaký výraz, nebo třeba volání funkce, zavolá se až když matchne, oproti řešení s polem.

    #3 pakl Tracy 2.6.8 ti s PHP 8 nefunguje?

    #4 vrana díky, opravil jsem to

    před měsícem · replied [6] pakl
  6. #5 David Grudl Tracy 2.6.8 samozrejme funguje. Dekuju a omlouvam se. Nejak mi nedoslo, ze tracy a latte vlastne nenasleduji major verzi Nette

    před měsícem · replied [7] David Grudl
  7. #6 pakl tak paráda. Už nikde starší Nette neopoužívám, tak mi mohlo samozřejmě něco uniknout.

    před měsícem
  8. #2 Polki Diky Davide za hezké vysvětlení.

    před měsícem
  9. 💙

    před měsícem

Chcete-li odeslat komentář, přihlaste se