PHP 8.0: Prezentare completă a noutăților (1/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 prim articol, vom analiza ce aduce nou din punct de vedere lingvistic.

Înainte de a ne scufunda în PHP, să știți că versiunea actuală a Nette este complet pregătită pentru versiunea 8. Mai mult, ca un cadou, a fost lansată și Nette 2.4, care este complet compatibilă cu aceasta, așa că din punctul de vedere al framework-ului, nimic nu vă împiedică să începeți să utilizați noua versiune.

Argumente denumite

Și începem direct cu o bombă, care poate fi considerată cu îndrăzneală un game changer. Acum se pot transmite argumente funcțiilor și metodelor nu doar pozițional, ci și după nume. Ceea ce este absolut grozav în cazul în care metoda are într-adevăr mulți parametri:

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

Primele două argumente le transmitem pozițional, următoarele după nume: (cele denumite trebuie să urmeze întotdeauna după cele poziționale)

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

// în loc de nebunia
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Înainte de apariția acestei caracteristici, era planificat să se creeze în Nette un nou API pentru trimiterea cookie-urilor, deoarece numărul de parametri setCookie() a crescut într-adevăr, iar notația pozițională era neclară. Acum nu mai este necesar, deoarece argumentele denumite sunt în acest caz cel mai practic API. IDE-urile le vor sugera și au verificare de tip.

Se potrivesc excelent și pentru a explica parametrii logici, unde utilizarea lor nu este necesară, dar true sau false singure nu spun prea multe:

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

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

Numele parametrilor devin acum parte a API-ului public. Nu este posibil să le schimbați liber ca până acum. Din acest motiv, și Nette trece printr-un audit pentru a verifica dacă toți parametrii au denumiri adecvate.

Argumentele denumite pot fi utilizate și în combinație cu variadics:

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

variadics(a: 1, b: 2, c: 3);
// în $args va fi ['b' => 2, 'c' => 3]

Acum, array-ul $args poate conține și chei nenumerice, ceea ce reprezintă o anumită modificare a compatibilității (BC break). Același lucru se aplică și comportamentului funcției call_user_func_array($func, $args), unde acum cheile din array-ul $args joacă un rol semnificativ. În schimb, funcțiile din familia func_*() sunt izolate de argumentele denumite.

Argumentele denumite sunt, de asemenea, strâns legate de faptul că operatorul splat ... poate acum despacheta și array-uri asociative:

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

În mod surprinzător, acest lucru nu funcționează încă în interiorul array-urilor:

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

Combinația de argumente denumite și variadics oferă posibilitatea de a avea în sfârșit o sintaxă fixă, de exemplu, pentru metoda presenterului link(), căreia acum îi putem transmite argumente denumite la fel ca cele poziționale:

// înainte
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // trebuia să fie un array

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

Sintaxa pentru argumente numite este mult mai sexy decât scrierea de array-uri, așa că Latte a adoptat-o imediat, unde poate fi utilizată, de exemplu, în etichetele {include} și {link}:

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

Vom reveni la parametrii denumiți în partea a treia, în legătură cu atributele.

Expresia poate arunca o excepție

Aruncarea unei excepții este acum o expresie. O puteți, de exemplu, încadra între paranteze și adăuga într-o condiție if. Hmmm, asta nu sună prea practic. Dar acest lucru este deja mai interesant:

// înainte
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('valoarea nu este setată');
}
$value = $arr['value'];


// acum, când throw este o expresie
$value = $arr['value'] ?? throw new \InvalidArgumentException('valoarea nu este setată');

Deoarece funcțiile săgeată (arrow functions) pot conține până acum doar o singură expresie, datorită acestei caracteristici pot arunca excepții:

// doar o singură expresie
$fn = fn() => throw new \Exception('hopa');

Expresii Match

Construcția switch-case are două mari defecte:

  • folosește compararea nestrictă == în loc de ===
  • trebuie să aveți grijă să nu uitați accidental de break

Prin urmare, PHP vine cu o alternativă sub forma noii construcții match, care utilizează compararea strictă și, în schimb, nu utilizează break.

Exemplu de cod switch:

switch ($statusCode) {
    case 200:
    case 300:
        $message = $this->formatMessage('ok');
        break;
    case 400:
        $message = $this->formatMessage('nu a fost găsit');
        break;
    case 500:
        $message = $this->formatMessage('eroare de server');
        break;
    default:
        $message = 'cod de stare necunoscut';
        break;
}

Și același lucru (doar cu comparare strictă) scris folosind match:

$message = match ($statusCode) {
    200, 300 => $this->formatMessage('ok'),
    400 => $this->formatMessage('nu a fost găsit'),
    500 => $this->formatMessage('eroare de server'),
    default => 'cod de stare necunoscut',
};

Observați că match nu este o structură de control ca switch, ci o expresie. Valoarea sa rezultată în exemplu o atribuim unei variabile. În același timp, și opțiunile individuale sunt expresii, deci nu se pot scrie mai mulți pași, ca în cazul switch.

Dacă nu există nicio potrivire cu nicio opțiune (și nu există clauza default), se aruncă excepția UnhandledMatchError.

Apropo, în Latte există și tag-urile {switch}, {case} și {default}. Funcționarea lor corespunde exact noului match. Folosesc compararea strictă, nu necesită break și în case este posibil să se specifice mai multe valori separate prin virgulă.

Operatorul Nullsafe

Înșiruirea opțională (optional chaining) permite scrierea unei expresii a cărei evaluare se oprește dacă întâlnește null. Și asta datorită noului operator ?->. Înlocuiește mult cod care altfel ar verifica în mod repetat null:

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

// înseamnă aproximativ
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

De ce „înseamnă aproximativ”? Deoarece, în realitate, expresia se evaluează mai inteligent și niciun pas nu se repetă. De exemplu, $user->getAddress() se apelează o singură dată, deci nu poate apărea problema cauzată de faptul că metoda ar returna ceva diferit prima și a doua oară.

Această nouă facilitate a fost introdusă acum un an de Latte. Acum ajunge în PHP însuși. Superb.

Promovarea proprietăților constructorului

Zahăr sintactic care economisește scrierea dublă a tipului și scrierea cvadruplă a variabilei. Păcat doar că nu a venit într-o perioadă în care nu aveam IDE-uri atât de inteligente, care astăzi scriu asta pentru noi 🙂

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

Funcționează cu Nette DI, puteți începe să o utilizați imediat.

Comportament strict al operatorilor aritmetici și pe biți

Ceea ce a propulsat odată limbajele de scripting dinamice în prim-plan a devenit în timp punctul lor cel mai slab. PHP s-a debarasat odată de “magic quotes”, înregistrarea variabilelor globale, iar acum comportamentul relaxat este înlocuit de strictețe. Vremea în care în PHP puteai aduna, înmulți etc. aproape orice tipuri de date pentru care acest lucru nu avea absolut niciun sens a trecut de mult. Începând cu versiunea 7.0, PHP devine din ce în ce mai strict, iar de la versiunea 8.0, încercarea de a utiliza orice operator aritmetic/pe biți la array-uri, obiecte sau resurse se termină cu TypeError. Excepția este adunarea array-urilor.

// operatori aritmetici și pe biți
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Comparare mai rezonabilă a șirurilor și numerelor

Sau make loose operator great again.

S-ar părea că pentru operatorul loose == nu mai există loc, că este doar o greșeală de tipar la scrierea ===, dar această modificare îl readuce pe hartă. Dacă tot îl avem, să se comporte rezonabil. Consecința comparării anterioare “nerezonabile” a fost, de exemplu, comportamentul in_array(), care vă putea păcăli neplăcut:

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

dump(in_array($value, $validValues));
// surprinzător, returna true
// de la PHP 8.0 returnează false

Modificarea comportamentului == se referă la compararea numerelor și a șirurilor “numerice” și este ilustrată de următorul tabel:

Comparare Înainte 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

Este surprinzător că este vorba de o modificare a compatibilității (BC break) chiar la baza limbajului, care a fost aprobată fără nicio opoziție. Și asta este bine. Aici JavaScript ar putea invidia mult.

Raportarea erorilor

Multe funcții interne aruncă acum TypeError și ValueError în loc de avertismente, care puteau fi ușor trecute cu vederea. Au fost reclasificate o serie de avertismente ale nucleului. Operatorul shutup @ nu mai reduce la tăcere erorile fatale. Și PDO în modul implicit aruncă excepții.

Aceste lucruri Nette a încercat întotdeauna să le rezolve într-un fel. Tracy modifica comportamentul operatorului shutup, Database schimba comportamentul PDO, Utils conține înlocuitori pentru funcțiile standard care aruncă excepții în loc de avertismente discrete etc. Este plăcut să vedem că direcția strictă, pe care Nette o are în ADN-ul său, devine direcția nativă a limbajului.

Array-uri cu index negativ

$arr[-5] = 'primul';
$arr[] = 'al doilea';

Care va fi cheia celui de-al doilea element? Înainte era 0, de la PHP 8 este -4.

Virgulă finală

Ultimul loc unde nu putea fi o virgulă finală era definirea parametrilor funcției. Acum este de domeniul trecutului:

	public function __construct(
		Nette\Database\Connection $db,
		Nette\Mail\Mailer $mailer, // virgulă finală
	) {
		....
	}

$object::class

Constanta magică ::class funcționează și cu obiecte $object::class, înlocuind complet funcția get_class().

catch fără variabilă

Și în final: în clauza catch nu este necesar să se specifice variabila pentru excepție:

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

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

În următoarele părți ne așteaptă noutăți esențiale în tipurile de date, vom arăta ce sunt atributele, ce funcții și clase noi au apărut în PHP și vom prezenta Just in Time Compiler.