PHP 8.0: Panoramica completa delle novità (1/4)

4 anni fa Da David Grudl  

È stata rilasciata la versione 8.0 di PHP. È così ricca di novità come nessuna versione precedente. La loro presentazione ha richiesto ben quattro articoli separati. In questo primo, vedremo cosa c'è di nuovo dal punto di vista del linguaggio.

Prima di immergerci in PHP, sappi che l'attuale versione di Nette è completamente pronta per l'otto. Inoltre, come regalo, è stata rilasciata anche Nette 2.4, che è pienamente compatibile con essa, quindi dal punto di vista del framework, nulla ti impedisce di iniziare a utilizzare la nuova versione.

Argomenti nominati

E iniziamo subito con una bomba, che può essere audacemente definita un game changer. Ora è possibile passare argomenti a funzioni e metodi non solo posizionalmente, ma anche per nome. Il che è assolutamente fantastico nel caso in cui un metodo abbia davvero molti 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
	) {
		...
	}
}

Passiamo i primi due argomenti posizionalmente, gli altri per nome: (quelli nominati devono sempre seguire quelli posizionali)

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

// invece del folle
$response->setCookie('lang', $lang, null, null, null, null, null, 'None');

Prima dell'arrivo di questa feature, era in programma creare una nuova API in Nette per l'invio di cookie, poiché il numero di parametri di setCookie() era davvero cresciuto e la notazione posizionale era poco chiara. Ora non è più necessario, perché gli argomenti nominati sono in questo caso l'API più pratica. L'IDE li suggerirà e hanno il controllo dei tipi.

Sono ottimi anche per chiarire i parametri logici, dove il loro uso non è necessario, ma true o false da soli dicono poco:

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

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

I nomi dei parametri diventano ora parte dell'API pubblica. Non è possibile cambiarli arbitrariamente come prima. Per questo motivo, anche Nette sta subendo un audit per verificare che tutti i parametri abbiano una denominazione appropriata.

Gli argomenti nominati possono essere utilizzati anche in combinazione con i variadics:

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

variadics(a: 1, b: 2, c: 3);
// in $args ci sarà ['b' => 2, 'c' => 3]

Ora l'array $args può contenere anche chiavi non numeriche, il che costituisce un certo BC break. Lo stesso vale per il comportamento della funzione call_user_func_array($func, $args), dove ora le chiavi nell'array $args giocano un ruolo significativo. Al contrario, le funzioni della famiglia func_*() sono schermate dagli argomenti nominati.

Agli argomenti nominati è strettamente correlato anche il fatto che l'operatore splat ... può ora espandere anche array associativi:

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

Stranamente, questo non funziona ancora all'interno degli array:

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

La combinazione di argomenti nominati e variadics offre finalmente la possibilità di avere una sintassi fissa, ad esempio per il metodo del presenter link(), al quale ora possiamo passare argomenti nominati così come posizionali:

// prima
$presenter->link('Product:detail', $id, 1, 2);
$presenter->link('Product:detail', [$id, 'page' => 1]); // doveva essere un array

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

La sintassi per i parametri nominati è molto più sexy della scrittura di array, motivo per cui è stata immediatamente adottata da Latte, dove può essere utilizzata ad esempio nei tag {include} e {link}:

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

Torneremo sui parametri nominati nella terza parte in relazione agli attributi.

Un'espressione può lanciare un'eccezione

Lanciare un'eccezione è ora un'espressione. Puoi, ad esempio, racchiuderla tra parentesi e aggiungerla a una condizione if. Hmmm, non sembra molto pratico. Ma questo è già più interessante:

// prima
if (!isset($arr['value'])) {
	throw new \InvalidArgumentException('valore non impostato');
}
$value = $arr['value'];


// ora, quando throw è un'espressione
$value = $arr['value'] ?? throw new \InvalidArgumentException('valore non impostato');

Poiché le arrow function possono finora contenere solo una singola espressione, grazie a questa feature possono lanciare eccezioni:

// solo una singola espressione
$fn = fn() => throw new \Exception('oops');

Match Expressions

Il costrutto switch-case ha due grandi difetti:

  • utilizza un confronto non rigoroso == invece di ===
  • devi fare attenzione a non dimenticare accidentalmente break

PHP offre quindi un'alternativa sotto forma del nuovo costrutto match, che utilizza un confronto rigoroso e, al contrario, non utilizza break.

Esempio di codice 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;
}

E lo stesso (solo con confronto rigoroso) scritto usando match:

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

Nota che match non è una struttura di controllo come switch, ma un'espressione. Nel nostro esempio, assegniamo il suo valore risultante a una variabile. Allo stesso tempo, anche le singole “opzioni” sono espressioni, quindi non è possibile scrivere più passaggi, come nel caso di switch.

Se non c'è corrispondenza con nessuna delle opzioni (e non esiste una clausola default), viene lanciata un'eccezione UnhandledMatchError.

A proposito, anche in Latte esistono i tag {switch}, {case} e {default}. Il loro funzionamento corrisponde esattamente al nuovo match. Utilizzano un confronto rigoroso, non richiedono break e in case è possibile specificare più valori separati da virgole.

Operatore Nullsafe

Il concatenamento opzionale (optional chaining) consente di scrivere un'espressione la cui valutazione si interrompe se incontra null. E questo grazie al nuovo operatore ?->. Sostituirà molto codice che altrimenti controllerebbe ripetutamente null:

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

// significa circa
$user !== null && $user->getAddress() !== null
	? $user->getAddress()->street
	: null

Perché “significa circa”? Perché in realtà l'espressione viene valutata in modo più intelligente e nessun passaggio viene ripetuto. Ad esempio, $user->getAddress() viene chiamato solo una volta, quindi non può sorgere il problema causato dal fatto che il metodo restituirebbe qualcosa di diverso la prima e la seconda volta.

Questa fresca novità è stata introdotta un anno fa da Latte. Ora arriva in PHP stesso. Fantastico.

Promozione delle proprietà del costruttore

Zucchero sintattico che risparmia la doppia scrittura del tipo e la quadrupla scrittura della variabile. Peccato solo che non sia arrivato quando non avevamo IDE così intelligenti, che oggi lo scrivono per 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,
	) {}
}

Funziona con Nette DI, puoi iniziare a usarlo subito.

Comportamento rigoroso degli operatori aritmetici e bitwise

Ciò che un tempo ha portato alla ribalta i linguaggi di scripting dinamici, col tempo è diventato il loro punto più debole. PHP si è sbarazzato delle “magic quotes”, della registrazione delle variabili globali e ora il comportamento permissivo viene sostituito dalla rigorosità. L'epoca in cui in PHP si potevano sommare, moltiplicare, ecc. quasi tutti i tipi di dati per i quali non aveva alcun senso è finita da tempo. A partire dalla versione 7.0, PHP è diventato sempre più rigoroso e dalla versione 8.0, il tentativo di utilizzare qualsiasi operatore aritmetico/bitwise su array, oggetti o resources termina con un TypeError. L'eccezione è la somma di array.

// operatori aritmetici e bitwise
+, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, --:

Confronto più ragionevole tra stringhe e numeri

Ovvero, rendiamo di nuovo grande l'operatore loose.

Sembrerebbe che per l'operatore loose == non ci sia più posto, che sia solo un errore di battitura nello scrivere ===, ma questa modifica lo riporta sulla mappa. Visto che ce l'abbiamo, che si comporti in modo ragionevole. La conseguenza del precedente confronto “irragionevole” era, ad esempio, il comportamento di in_array(), che poteva giocarvi un brutto scherzo:

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

dump(in_array($value, $validValues));
// sorprendentemente restituiva true
// da PHP 8.0 restituisce false

La modifica nel comportamento di == riguarda il confronto tra numeri e stringhe “numeriche” ed è mostrata nella seguente tabella:

Confronto Prima 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

Sorprendente è che si tratta di un BC break nel fondamento stesso del linguaggio, che è stato approvato senza alcuna opposizione. E questo è un bene. Qui JavaScript potrebbe invidiare molto.

Segnalazione degli errori

Molte funzioni interne ora sollevano TypeError e ValueError invece di avvisi, che potevano essere facilmente trascurati. Sono stati riclassificati numerosi avvisi del core. L'operatore shutup @ ora non silenzia gli errori fatali. E PDO nella modalità predefinita lancia eccezioni.

Nette ha sempre cercato di risolvere queste cose in qualche modo. Tracy modificava il comportamento dell'operatore shutup, Database cambiava il comportamento di PDO, Utils contiene sostituti delle funzioni standard che lanciano eccezioni invece di avvisi discreti, ecc. È bello vedere che la direzione rigorosa, che Nette ha nel suo DNA, sta diventando la direzione nativa del linguaggio.

Array con indice negativo

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

Quale sarà la chiave del secondo elemento? Prima era 0, da PHP 8 è -4.

Virgola finale

L'ultimo posto dove non poteva esserci una virgola finale era la definizione dei parametri della funzione. Questo è ormai passato:

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

$object::class

La costante magica ::class funziona anche con gli oggetti $object::class, sostituendo completamente la funzione get_class().

catch senza variabile

E infine: nella clausola catch non è necessario specificare la variabile per l'eccezione:

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

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

Nelle prossime parti ci aspettano novità fondamentali nei tipi di dati, mostreremo cosa sono gli attributi, quali nuove funzioni e classi sono apparse in PHP e presenteremo il Just in Time Compiler.