PHP 8.0: Teljes áttekintés az újdonságokról (1/4)

3 éve A címről David Grudl  

A PHP 8.0-s verziója most jelenik meg. Tele van olyan újdonságokkal, mint egyetlen korábbi verzió sem. A bevezetésük négy külön cikket érdemelt volna. Az elsőben megnézzük, hogy mit hoz nyelvi szinten.

Mielőtt belemerülnénk a PHP-ba, tudassuk, hogy a Nette jelenlegi verziója teljesen felkészült a nyolcadik verzióra. Sőt, ajándékba kiadták a teljesen kompatibilis Nette 2.4-et is, így keretrendszer szempontjából semmi akadálya nincs a használatának.

Megnevezett érvek

Kezdjük rögtön egy bombával, amit bátran nevezhetnénk játékváltónak. Az argumentumokat most már nem csak pozicionálisan, hanem nevük szerint is átadhatjuk függvényeknek és metódusoknak. Ami abszolút menő abban az esetben, ha egy metódusnak tényleg túl sok paramétere van:

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

Az első két argumentumot pozicionálisan adjuk át, a többit név szerint: (a név szerinti argumentumoknak a pozicionális után kell következniük).

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

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

Ennek a funkciónak a bevezetése előtt tervben volt egy új API létrehozása a Nette-ben a sütik küldésére, mivel a setCookie() érvek száma nagyon megnőtt, és a pozicionális jelölés zavaró volt. Erre már nincs szükség, mert a név szerinti argumentumok ebben az esetben a legkényelmesebb API-t jelentik. Az IDE utalni fog rájuk, és van típusbiztonság.

Ideálisak még a logikai argumentumok magyarázatára is, ahol nem szükséges a használatuk, de egy sima true vagy false nem vágja meg a dolgot:

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

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

Az argumentumnevek mostantól a nyilvános API részei. Már nem lehet őket tetszés szerint megváltoztatni. Emiatt még a Nette is átesik egy ellenőrzésen, amely meghatározza, hogy minden argumentumnak van-e megfelelő neve.

A megnevezett argumentumok variadikkal kombinálva is használhatók:

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

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

A $args tömb most már nem numerikus kulcsokat is tartalmazhat, ami egyfajta BC-szakadás. Ugyanez vonatkozik a call_user_func_array($func, $args) viselkedésére is, ahol a $args tömbben lévő kulcsok most sokkal jelentősebb szerepet játszanak. Ezzel szemben a func_*() család függvényei védve vannak a nevesített argumentumoktól.

A nevesített argumentumok szorosan kapcsolódnak ahhoz a tényhez, hogy a ... splat operátor most már asszociatív tömböket is bővíthet:

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

Meglepő módon jelenleg nem működik tömbökön belül:

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

A nevesített argumentumok és a variadics kombinációja lehetőséget ad arra, hogy végre legyen egy fix szintaxis, például a link() metódushoz, amelynek átadhatunk nevesített és pozicionális argumentumokat is:

// 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);

A megnevezett argumentumok szintaxisa sokkal szexibb, mint a tömbök írása, ezért a Latte azonnal átvette, ahol például a {include} és a {link} címkékben használható:

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

A sorozat harmadik részében az attribútumok kapcsán visszatérünk a névvel ellátott argumentumokra.

Egy kifejezés kivételt dobhat

Egy kivétel dobása most már egy kifejezés. Zárójelekbe zárva és a if feltételhez hozzáadható. Hmmm, ez nem hangzik túl praktikusan. Ez azonban sokkal érdekesebb:

// 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');

Mivel a nyílfüggvények eddig csak egy kifejezést tartalmazhattak, ennek a funkciónak köszönhetően mostantól kivételeket dobhatnak:

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

Match kifejezés

A switch-case utasításnak két fő hibája van:

  • nem szigorú összehasonlítást használ == helyett. ===
  • vigyázni kell, nehogy véletlenül elfelejtsük a break címet.

Emiatt a PHP egy alternatívát kínál az új match kifejezés formájában, amely szigorú összehasonlítást használ, és fordítva, nem használja a break címet.

switch kódpélda:

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 match használatával íródott ugyanez (csak szigorú összehasonlítással):

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

Megjegyzendő, hogy a match nem egy vezérlési struktúra, mint a switch, hanem egy kifejezés. A példában a kapott értéket egy változóhoz rendeljük. Ugyanakkor az egyes “opciók” is kifejezések, így nem lehet több lépést írni, mint a switch esetében.

Ha nincs találat (és nincs alapértelmezett záradék), akkor a UnhandledMatchError kivételt dobja.

Egyébként a Latte-ban vannak {switch}, {case} és {default} címkék is. Ezek funkciója pontosan megfelel az új match. Szigorú összehasonlítást használnak, nem igénylik a break címet, és lehetőség van több, vesszővel elválasztott érték megadására a case címen.

Nullsafe operátor

Az opcionális láncolás lehetővé teszi, hogy olyan kifejezést írjunk, amelynek kiértékelése leáll, ha null értékkel találkozik. Ez az új ?-> operátornak köszönhető. Sok olyan kódot helyettesít, amely egyébként többször is ellenőrizni kellene a null értéket:

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

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

Miért “megközelítőleg”? Mert a valóságban a kifejezés kiértékelése leleményesebben történik, így egyetlen lépés sem ismétlődik. A $user->getAddress() például csak egyszer hívódik meg, így nem fordulhat elő az a probléma, amit az okoz, hogy a módszer első és második alkalommal mást ad vissza.

Ezt a funkciót Latte:https://blog.nette.org/…om-functions hozta egy évvel ezelőtt. Most már maga a PHP is átveszi. Nagyszerű.

Konstruktor tulajdonság promóció

Szintaktikai cukor, amely megspórolja a típus kétszeri és a változó négyszeri leírását. Kár, hogy nem akkor jött, amikor még nem voltak ilyen okos IDE-k, amik ma megírják helyettünk 🙂

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

A Nette DI-vel működik, elkezdheted használni.

Szigorúbb típusellenőrzés az aritmetikai és bitenkénti operátorok esetében.

Ami egykor a dinamikus szkriptnyelvek felemelkedését segítette, mára a leggyengébb pontjukká vált. Egykor a PHP megszabadult a “mágikus idézőjelektől”, a globális változók regisztrációjától, most pedig a laza viselkedést felváltja a szigorúság. Az az idő, amikor a PHP-ben szinte bármilyen adattípust összeadhattunk, szorozhattunk stb. aminek semmi értelme nem volt, már régen elmúlt. A 7.0-s verzióval kezdődően a PHP egyre szigorúbbá válik, és a 8.0-s verzió óta bármilyen aritmetikai/bitwise operátor használata tömbökön, objektumokon vagy erőforrásokon TypeErrorral végződik. Kivételt képeznek a tömbök hozzáadása.

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

Józanabb string-szám összehasonlítások

Vagy tegye újra naggyá a laza operátort.

Úgy tűnik, hogy a laza operátornak már nincs helye ==, hogy csak egy elírás a === írásakor, de ez a változtatás ismét visszahelyezi a térképre. Ha már megvan, viselkedjen ésszerűen. A korábbi “ésszerűtlen” összehasonlítás eredményeként például a in_array() kellemetlenül trollkodhat:

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

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

A == viselkedésének változása a számok és a “numerikus” karakterláncok összehasonlítására vonatkozik, és a következő táblázatban látható:

Összehasonlítás Előzőleg 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

Meglepő módon ez egy BC törés a nyelv legmélyén, amit ellenállás nélkül jóváhagytak. És ez jó. A JavaScript ebben a tekintetben nagyon irigylésre méltó lehetne.

Hibajelentés

Sok belső függvény mostantól TypeError és ValueError hibát vált ki a korábban könnyen figyelmen kívül hagyható figyelmeztetések helyett. Számos kernel figyelmeztetés át lett osztályozva. A @ leállítás operátor mostantól nem hallgatja el a végzetes hibákat. És a PDO alapértelmezés szerint dobja a kivételeket.

A Nette mindig igyekezett ezeket a dolgokat valamilyen módon megoldani. A Tracy módosította a shutup operátor viselkedését, az adatbázis átállította a PDO viselkedését, az Utils tartalmazza a szabványos függvények helyettesítését, amelyek a könnyen kihagyható figyelmeztetések helyett kivételeket dobnak, stb. Jó látni, hogy a Nette DNS-ében lévő szigorú irány a nyelv natív irányává válik.

Negatív tömbkulcs növekményei

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

Mi lesz a második elem kulcsa? Régebben 0, since PHP 8 it’s -4 volt.

Utolsó vessző

Az utolsó hely, ahol a vessző nem lehetett, a függvény argumentumainak meghatározása volt. Ez már a múlté:

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

$object::class

A ::class mágikus konstans a $object::class objektumokkal is működik, teljesen helyettesítve a get_class() függvényt.

Kivételek fogása csak típus szerint

És végül: a catch záradékban nem szükséges változót megadni a kivételhez:

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

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

A következő részekben az adattípusokkal kapcsolatos főbb újdonságokat látjuk, megmutatjuk, hogy 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.