PHP 8.0: Funcții noi, clase și JIT (4/4)

acum 4 ani de David Grudl  

A fost lansată versiunea PHP 8.0. Este atât de plină de noutăți cum nu a mai fost nicio versiune înainte. Prezentarea lor a necesitat patru articole separate. În acest ultim articol, vom analiza funcțiile și clasele noi și vom prezenta Just in Time Compiler.

Funcții noi

Biblioteca standard PHP dispune de sute de funcții, iar în versiunea 8.0 au apărut șase noi. Pare puțin, dar majoritatea dintre ele acoperă punctele slabe ale limbajului. Ceea ce corespunde frumos cu tonul întregii versiuni 8.0, care definitivează și consolidează PHP ca nicio versiune anterioară. O prezentare generală a tuturor funcțiilor și metodelor noi o găsiți în ghidul de migrare.

str_contains() str_starts_with() str_ends_with()

Funcții pentru a determina dacă un șir începe, se termină sau conține un subșir.

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

Odată cu apariția acestui trio, PHP definește cum să se trateze șirul gol la căutare, conform căruia se ghidează și toate celelalte funcții conexe, și anume că șirul gol se găsește peste tot:

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (anterior false)

Datorită acestui fapt, comportamentul trio-ului este complet identic cu echivalentele din Nette:

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

De ce sunt aceste funcții atât de importante? Bibliotecile standard ale tuturor limbajelor sunt întotdeauna împovărate de dezvoltarea istorică și nu se poate evita apariția inconsecvențelor și a pașilor greșiți. Dar, în același timp, este cartea de vizită a limbajului respectiv. Este surprinzător când la PHP, vechi de 25 de ani, lipsesc funcții pentru operații atât de fundamentale, cum ar fi returnarea primului sau ultimului element dintr-un array, escaparea HTML fără capcane (htmlspecialchars nu escapează apostroful) sau tocmai căutarea unui șir într-un șir. Faptul că se poate ocoli cumva nu rezistă, deoarece rezultatul nu este un cod lizibil și inteligibil. Este o lecție pentru toți autorii de API-uri. Când vedeți că o parte considerabilă a documentației unei funcții este ocupată de explicarea subtilităților (cum ar fi valorile returnate de strpos), este un semnal clar pentru a modifica biblioteca și a adăuga tocmai str_contains.

get_debug_type()

Înlocuiește deja învechitul get_type(). În loc de tipuri lungi precum integer, returnează int utilizat astăzi, în cazul obiectelor returnează direct tipul:

Valoare 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)

Obiectivizarea resurselor

Valorile de tip resource provin din vremurile în care PHP nu avea încă obiecte, dar de fapt avea nevoie de ele. Așa au apărut resursele. Astăzi avem obiecte și, spre deosebire de resurse, funcționează mult mai bine cu garbage collector-ul, așa că planul este să le înlocuim treptat pe toate cu obiecte.

De la PHP 8.0, resursele imagini, conexiuni curl, openssl, xml, etc. se transformă în obiecte. În PHP 8.1, vor urma conexiunile FTP etc.

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

Aceste obiecte nu au deocamdată nicio metodă și nici nu puteți crea direct instanțele lor. Este vorba deocamdată doar de a scoate din PHP resursele învechite fără a schimba API-ul. Și asta este bine, deoarece crearea unui API bun este o sarcină separată și dificilă. Nimeni nu dorește ca în PHP să apară alte clase precum SplFileObject cu metode denumite fgetc() sau fgets().

PhpToken

Și tokenizerul se mută în obiecte, deci funcțiile din jurul token_get_all. De data aceasta nu este vorba de eliminarea resurselor, ci primim un obiect complet care reprezintă un token PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instanță PhpToken

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

Metoda isIgnorable() returnează true pentru tokenurile T_WHITESPACE, T_COMMENT, T_DOC_COMMENT și T_OPEN_TAG.

Weak maps

Weak map-urile sunt legate de garbage collector, care eliberează din memorie toate obiectele și valorile care nu mai sunt utilizate (adică nu există nicio variabilă sau proprietate utilizată care să le conțină). Deoarece viața unui fir de execuție PHP este efemeră și astăzi avem suficientă memorie disponibilă pe servere, de obicei nu ne preocupăm deloc de problemele legate de eliberarea eficientă a memoriei. Dar pentru scripturile care rulează pe termen lung, acestea sunt esențiale.

Obiectul WeakMap este similar cu SplObjectStorage. În ambele, obiectele sunt utilizate ca chei și permit stocarea oricăror valori sub ele. Diferența este că WeakMap nu împiedică eliberarea obiectului de către garbage collector. Adică, dacă singurul loc în care obiectul mai există este cheia într-un weak map, acesta va fi eliminat din mapă și din memorie.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'date pentru $obj';

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

La ce este bun acest lucru? De exemplu, pentru caching. Să presupunem că avem o metodă loadComments(), căreia îi transmitem un articol de blog și ea returnează toate comentariile sale. Deoarece metoda este apelată în mod repetat cu același articol, vom crea și getComments(), care va stoca în cache rezultatul primei metode:

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

	...
}

Ideea este că în momentul în care obiectul $article este eliberat (de exemplu, aplicația începe să lucreze cu alt articol), se eliberează și intrarea sa din cache.

PHP JIT (Just in Time Compiler)

Poate știți că PHP se compilează în așa-numitul opcode, care sunt instrucțiuni de nivel scăzut, pe care le puteți vizualiza, de exemplu, aici și care sunt executate de mașina virtuală PHP. Și ce este JIT? JIT poate compila transparent PHP direct în cod mașină, care este executat direct de procesor, ocolind astfel execuția mai lentă de către mașina virtuală.

Deci, JIT ar trebui să accelereze PHP.

Efortul de a implementa JIT în PHP datează din 2011 și îi aparține lui Dmitry Stogov. De atunci, a încercat 3 implementări diferite, dar niciuna dintre ele nu a ajuns în PHP-ul oficial din următoarele motive: rezultatul nu a fost niciodată o creștere semnificativă a performanței pentru aplicațiile web tipice; complică întreținerea PHP (adică nimeni în afară de Dmitry nu înțelege 😉); existau alte modalități de a îmbunătăți performanța fără a fi nevoie să se utilizeze JIT.

Creșterea bruscă a performanței PHP în versiunea 7 a fost un produs secundar al lucrului la JIT, deși, paradoxal, nu a fost implementat. Acest lucru se întâmplă abia în PHP 8. Dar voi tempera imediat așteptările exagerate: probabil nu veți observa nicio accelerare.

De ce intră atunci JIT în PHP? Pe de o parte, alte căi de îmbunătățire a performanței se epuizează încet, iar JIT este pur și simplu la rând. Deși nu aduce accelerare în aplicațiile web obișnuite, accelerează semnificativ, de exemplu, calculele matematice. Astfel, se deschide posibilitatea de a începe să scriem aceste lucruri în PHP. Și, de fapt, ar fi posibil să implementăm direct în PHP funcții care până acum necesitau implementare direct în C din cauza vitezei.

JIT face parte din extensia opcache și se activează împreună cu aceasta în php.ini (citiți documentația despre cele patru cifre):

zend_extension=php_opcache.dll
opcache.jit=1205              ; configurare folosind cvartetul OTRC
opcache.enable_cli=1          ; pentru a funcționa și în CLI
opcache.jit_buffer_size=128M  ; memorie alocată pentru codul compilat

Că JIT rulează veți afla, de exemplu, în panoul informativ din Tracy Bar.

JIT funcționează foarte bine atunci când toate variabilele au tipuri clar definite și nu se pot schimba la apelarea repetată a aceluiași cod. Sunt curios, așadar, dacă vom declara odată în PHP tipuri și pentru variabile: string $s = 'Salut, acesta este finalul serialului';