PHP 8.0: Új függvények, osztályok és JIT (4/4)

4 éve írta David Grudl  

Megjelent a PHP 8.0-s verziója. Annyira tele van újdonságokkal, mint még egyetlen verzió sem korábban. Bemutatásukhoz egyenesen négy különálló cikkre volt szükség. Ebben az utolsóban az új függvényeket és osztályokat nézzük meg, és bemutatjuk a Just in Time Compilert.

Új függvények

A PHP standard könyvtára több száz függvényt tartalmaz, és a 8.0-s verzióban hat új jelent meg. Ez kevésnek tűnik, de többségük a nyelv gyenge pontjait pótolja. Ami szépen illeszkedik az egész 8.0-s verzió hangulatához, amely úgy tökéletesíti és konszolidálja a PHP-t, mint még egyetlen verzió sem korábban. Az összes új függvény és metódus áttekintését a migration guide-ban találja.

str_contains() str_starts_with() str_ends_with()

Függvények annak megállapítására, hogy egy string kezdődik-e, végződik-e, vagy tartalmaz-e egy részstringet.

if (str_contains('Nette', 'te')) {
	...
}

Ezzel a hármassal együtt a PHP definiálja, hogyan kell kezelni az üres stringet a keresés során, amihez az összes többi kapcsolódó függvény is igazodik, mégpedig úgy, hogy az üres string mindenhol megtalálható:

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (korábban false)

Ennek köszönhetően a hármas viselkedése teljesen azonos a Nette-beli megfelelőivel:

str_contains()      # Nette\Utils\String::contains()
str_starts_with()   # Nette\Utils\String::startsWith()
str_ends_with()     # Nette\Utils\String::endsWith()

Miért olyan fontosak ezek a függvények? Minden nyelv standard könyvtára mindig terhelt a történelmi fejlődéssel, és nem lehet elkerülni a következetlenségek és melléfogások kialakulását. De ugyanakkor ez az adott nyelv névjegye is. Meglepő, ha egy 25 éves PHP-ból hiányoznak a függvények olyan alapvető műveletekhez, mint a tömb első vagy utolsó elemének visszaadása, a HTML escapelése trükkök nélkül (htmlspecialchars nem escapeli az aposztrófot), vagy éppen a string keresése stringben. Az, hogy valahogy meg lehet kerülni, nem állja meg a helyét, mert az eredmény nem olvasható és érthető kód lesz. Ez tanulság minden API szerző számára. Ha látja, hogy a függvény dokumentációjának jelentős részét a buktatók magyarázata foglalja el (mint például a strpos visszatérési értékei), az egyértelmű jelzés a könyvtár módosítására és éppen a str_contains kiegészítésére.

get_debug_type()

Helyettesíti a már elavult get_type()-ot. Hosszú típusok helyett, mint az integer, a ma használt int-et adja vissza, objektumok esetén pedig egyenesen a típust:

Érték gettype() get_debug_type()
'abc' string string
[1, 2] array array
231 integer int
3.14 double float
true boolean bool
null NULL null
new stdClass object stdClass
new Foo\Bar object Foo\Bar
function() {} object Closure
new class {} object class@anonymous
new class extends Foo {} object Foo@anonymous
curl_init() resource resource (curl)
curl_close($ch) resource (closed) resource (closed)

Erőforrások objektumosítása

A resource típusú értékek abból az időből származnak, amikor a PHP-nak még nem voltak objektumai, de valójában szüksége volt rájuk. Így jöttek létre az erőforrások. Ma már vannak objektumaink, és az erőforrásokkal szemben sokkal jobban működnek a garbage collectorral, így a terv az, hogy fokozatosan mindet objektumokra cseréljük.

A PHP 8.0-tól kezdve objektumokká válnak a képek, curl kapcsolatok, openssl, xml stb. erőforrásai. A PHP 8.1-ben az FTP kapcsolatok stb. következnek.

$res = imagecreatefromjpeg('image.jpg');
$res instanceof GdImage  // true
is_resource($res)        // false - BC break

Ezeknek az objektumoknak még nincsenek metódusaik, és közvetlenül sem hozhat létre példányokat belőlük. Egyelőre valóban csak arról van szó, hogy megszabaduljunk a PHP elavult erőforrásaitól az API megváltoztatása nélkül. És ez jó, mert egy jó API létrehozása önálló és nehéz feladat. Senki sem szeretné, ha a PHP-ban további olyan osztályok jönnének létre, mint az SplFileObject, fgetc() vagy fgets() nevű metódusokkal.

PhpToken

Objektumokba kerül át a tokenizáló is, tehát a token_get_all körüli függvények. Ezúttal nem az erőforrásoktól való megszabadulásról van szó, hanem egy teljes értékű objektumot kapunk, amely egy PHP tokent reprezentál.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // PhpToken példány

echo $token->id;             // T_OPEN_TAG
echo $token->text;           // '<?php'
echo $token->line;           // 1
echo $token->getTokenName(); // 'T_OPEN_TAG'
echo $token->is(T_STRING);   // false
echo $token->isIgnorable();  // true

Az isIgnorable() metódus true-t ad vissza a T_WHITESPACE, T_COMMENT, T_DOC_COMMENT és T_OPEN_TAG tokenekre.

Weak map-ek

A weak map-ek a garbage collectorhoz kapcsolódnak, amely felszabadítja a memóriából az összes olyan objektumot és értéket, amelyeket már nem használnak (azaz nincs olyan használt változó vagy property, amely tartalmazná őket). Mivel a PHP szál élete kérészéletű, és ma már elegendő memória áll rendelkezésünkre a szervereken, a hatékony memóriafelszabadítás kérdéseivel általában egyáltalán nem foglalkozunk. De a hosszabb ideig futó szkripteknél alapvetőek.

Az WeakMap objektum hasonló az SplObjectStorage-hoz. Mindkettőben objektumokat használnak kulcsként, és lehetővé teszik tetszőleges értékek tárolását alattuk. A különbség az, hogy a WeakMap nem akadályozza meg, hogy az objektumot a garbage collector felszabadítsa. Azaz, ha az egyetlen hely, ahol az objektum még előfordul, a weak map kulcsa, akkor eltávolításra kerül a map-ből és a memóriából is.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'adatok $obj-hoz';

dump(count($map));  // 1
unset($obj);
dump(count($map));  // 0

Mire jó ez? Például cache-elésre. Legyen egy loadComments() metódusunk, amelynek átadunk egy blogcikket, és visszaadja annak összes kommentjét. Mivel a metódus ugyanazzal a cikkel ismételten meghívódik, létrehozunk még egy getComments()-t, amely az első metódus eredményét cache-eli:

class Comments
{
	private WeakMap $cache;

	public function __construct()
	{
		$this->cache = new WeakMap;
	}

	public function getComments(Article $article): ?array
	{
		$this->cache[$article] ??= $this->loadComments($article);
		return $this->cache[$article]
	}

	...
}

A trükk az, hogy abban a pillanatban, amikor az $article objektum felszabadul (például az alkalmazás elkezd egy másik cikkel dolgozni), annak bejegyzése is felszabadul a cache-ből.

PHP JIT (Just in Time Compiler)

Talán tudja, hogy a PHP ún. opcode-ra fordítódik, ami low-level utasítások, amelyeket megnézhet például itt, és amelyeket a PHP virtuális gépe hajt végre. És mi a JIT? A JIT képes a PHP-t transzparensen közvetlenül gépi kódra fordítani, amelyet közvetlenül a processzor hajt végre, így megkerüli a virtuális gép lassabb végrehajtását.

A JIT tehát felgyorsítja a PHP-t.

A JIT PHP-ba való implementálására irányuló törekvés egészen 2011-ig nyúlik vissza, és Dmitry Stogov áll mögötte. Azóta 3 különböző implementációt próbált ki, de egyik sem került be az éles PHP-ba, és ennek okai a következők: az eredmény soha nem volt jelentős teljesítménynövekedés a tipikus webalkalmazásoknál; bonyolítja a PHP karbantartását (azaz senki sem érti Dmitry-n kívül 😉); voltak más utak a teljesítmény javítására anélkül, hogy JIT-et kellett volna használni.

A PHP teljesítményének ugrásszerű növekedése a 7-es verzióban a JIT-en végzett munka mellékterméke volt, bár paradox módon nem került bevezetésre. Ez csak a PHP 8-ban történik meg. De rögtön visszafogom a túlzott elvárásokat: valószínűleg semmilyen gyorsulást nem fog észrevenni.

Miért lép tehát be a JIT a PHP-ba? Egyrészt a teljesítmény javításának más útjai lassan elfogynak, és a JIT egyszerűen sorra kerül. A szokásos webalkalmazásokban ugyan nem hoz gyorsulást, de alapvetően felgyorsítja például a matematikai számításokat. Így lehetőség nyílik arra, hogy ezeket a dolgokat PHP-ban kezdjük el írni. És valójában így közvetlenül PHP-ban lehetne implementálni olyan függvényeket, amelyek eddig a sebesség miatt közvetlenül C-ben történő implementációt igényeltek.

A JIT az opcache kiterjesztés része, és vele együtt kapcsolható be a php.ini-ben (olvassa el a dokumentációt ahhoz a négy számjegyhez):

zend_extension=php_opcache.dll
opcache.jit=1205              ; konfiguráció a négy OTRC számjeggyel
opcache.enable_cli=1          ; hogy CLI-ben is működjön
opcache.jit_buffer_size=128M  ; lefoglalt memória a lefordított kódhoz

Hogy a JIT fut, azt például a Tracy Bar információs paneljén tudhatja meg.

A JIT nagyon jól működik akkor, ha minden változónak világosan meghatározott típusa van, és nem változhatnak meg ugyanazon kód ismételt hívásakor. Kíváncsi vagyok tehát, hogy egyszer a PHP-ban a változóknál is deklarálunk-e majd típusokat: string $s = 'Sziasztok, ez a sorozat vége';