PHP 8.0: Nuove funzioni, classi e JIT (4/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 quest'ultima parte, esamineremo le nuove funzioni e classi e presenteremo il Just in Time Compiler.

Nuove funzioni

La libreria standard di PHP dispone di centinaia di funzioni e nella versione 8.0 ne sono apparse sei nuove. Sembra poco, ma la maggior parte di esse colma i punti deboli del linguaggio. Il che si sposa bene con il tono dell'intera versione 8.0, che perfeziona e consolida PHP come nessuna versione precedente. Una panoramica di tutte le nuove funzioni e metodi si trova nella migration guide.

str_contains() str_starts_with() str_ends_with()

Funzioni per verificare se una stringa inizia, finisce o contiene una sottostringa.

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

Insieme all'arrivo di questa tripletta, PHP definisce come trattare la stringa vuota durante la ricerca, criterio seguito anche da tutte le altre funzioni correlate, ovvero che la stringa vuota si trova ovunque:

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

Grazie a ciò, il comportamento della tripletta è del tutto identico agli equivalenti in Nette:

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

Perché queste funzioni sono così importanti? Le librerie standard di tutti i linguaggi sono sempre appesantite dallo sviluppo storico e non si può evitare la nascita di incoerenze e passi falsi. Ma allo stesso tempo, sono il biglietto da visita di quel linguaggio. È sorprendente che in un PHP vecchio di 25 anni manchino funzioni per operazioni così basilari come restituire il primo o l'ultimo elemento di un array, eseguire l'escaping HTML senza tranelli (htmlspecialchars non esegue l'escaping dell'apostrofo), o appunto cercare una stringa in una stringa. Che si possa in qualche modo aggirare non regge, perché il risultato non è un codice leggibile e comprensibile. È una lezione per tutti gli autori di API. Quando vedete che una parte considerevole della documentazione di una funzione è dedicata alla spiegazione delle insidie (come ad esempio i valori di ritorno di strpos), è un chiaro segnale per modificare la libreria e aggiungere proprio str_contains.

get_debug_type()

Sostituisce il già obsoleto get_type(). Invece di tipi lunghi come integer, restituisce l'ormai usato int, nel caso di oggetti restituisce direttamente il tipo:

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

Oggettivazione delle risorse

I valori di tipo resource provengono dai tempi in cui PHP non aveva ancora oggetti, ma in realtà ne aveva bisogno. Così sono nate le risorse. Oggi abbiamo gli oggetti e rispetto alle risorse funzionano molto meglio con il garbage collector, quindi il piano è di sostituirle gradualmente tutte con oggetti.

Da PHP 8.0, le risorse immagini, connessioni curl, openssl, xml, ecc. vengono trasformate in oggetti. In PHP 8.1 sarà la volta delle connessioni FTP, ecc.

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

Questi oggetti per ora non hanno metodi, né potete creare direttamente le loro istanze. Si tratta per ora davvero solo di eliminare da PHP le risorse obsolete senza modificare l'API. E questo è un bene, perché creare una buona API è un compito separato e impegnativo. Nessuno desidera che in PHP nascano altre classi come SplFileObject con metodi chiamati fgetc() o fgets().

PhpToken

Anche il tokenizer si sposta verso gli oggetti, e quindi le funzioni intorno a token_get_all. Questa volta non si tratta di sbarazzarsi delle risorse, ma otteniamo un oggetto a pieno titolo che rappresenta un singolo token PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // istanza 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

Il metodo isIgnorable() restituisce true per i token T_WHITESPACE, T_COMMENT, T_DOC_COMMENT e T_OPEN_TAG.

Weak maps

Le weak map sono correlate al gargabe collector, che rilascia dalla memoria tutti gli oggetti e i valori che non vengono più utilizzati (cioè non esiste alcuna variabile o proprietà utilizzata che li contenga). Poiché la vita di un thread PHP è effimera e oggi abbiamo molta memoria disponibile sui server, di solito non ci preoccupiamo affatto delle questioni relative al rilascio efficiente della memoria. Ma per gli script a lunga esecuzione sono fondamentali.

L'oggetto WeakMap è simile a SplObjectStorage. In entrambi, gli oggetti vengono utilizzati come chiavi e consentono di memorizzare sotto di essi valori arbitrari. La differenza è che WeakMap non impedisce che l'oggetto venga rilasciato dal garbage collector. Cioè, se l'unico posto in cui l'oggetto esiste ancora è la chiave nella weak map, verrà rimosso dalla map e dalla memoria.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'dati per $obj';

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

A cosa serve? Ad esempio, per il caching. Supponiamo di avere un metodo loadComments(), al quale passiamo un articolo del blog e che restituisce tutti i suoi commenti. Poiché il metodo viene chiamato ripetutamente con lo stesso articolo, creiamo anche getComments(), che metterà in cache il risultato del primo metodo:

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

	...
}

Il trucco sta nel fatto che nel momento in cui l'oggetto $article viene rilasciato (ad esempio, l'applicazione inizia a lavorare con un altro articolo), viene rilasciata anche la sua voce dalla cache.

PHP JIT (Just in Time Compiler)

Forse sapete che PHP viene compilato nel cosiddetto opcode, che sono istruzioni di basso livello, che potete vedere ad esempio qui e che vengono eseguite dalla macchina virtuale PHP. E cos'è JIT? JIT può compilare trasparentemente PHP direttamente in codice macchina, che viene eseguito direttamente dal processore, quindi si evita l'esecuzione più lenta tramite la macchina virtuale.

JIT dovrebbe quindi accelerare PHP.

Il tentativo di implementare JIT in PHP risale al 2011 ed è opera di Dmitry Stogov. Da allora ha provato 3 diverse implementazioni, ma nessuna di esse è arrivata in una versione stabile di PHP per i seguenti motivi: il risultato non è mai stato un aumento significativo delle prestazioni per le tipiche applicazioni web; complica la manutenzione di PHP (cioè nessuno tranne Dmitry lo capisce 😉); esistevano altri modi per migliorare le prestazioni senza dover usare JIT.

L'aumento di prestazioni di PHP nella versione 7 è stato un sottoprodotto del lavoro su JIT, sebbene paradossalmente non sia stato distribuito. Ciò avviene solo in PHP 8. Ma frenerò subito le aspettative eccessive: probabilmente non noterete alcuna accelerazione.

Perché allora JIT entra in PHP? Innanzitutto, altri modi per migliorare le prestazioni si stanno lentamente esaurendo e JIT è semplicemente il prossimo passo. Nelle normali applicazioni web non porta accelerazioni, ma accelera significativamente, ad esempio, i calcoli matematici. Si apre così la possibilità di iniziare a scrivere queste cose in PHP. E in realtà sarebbe possibile implementare direttamente in PHP funzioni che finora richiedevano l'implementazione direttamente in C per motivi di velocità.

JIT fa parte dell'estensione opcache e si attiva insieme ad essa in php.ini (leggi la documentazione relativa a quella quaterna di cifre):

zend_extension=php_opcache.dll
opcache.jit=1205              ; configurazione tramite la quaterna OTRC
opcache.enable_cli=1          ; affinché funzioni anche in CLI
opcache.jit_buffer_size=128M  ; memoria riservata per il codice compilato

Che JIT sia in esecuzione lo scoprirete ad esempio nel pannello informativo nella Tracy Bar.

JIT funziona molto bene quando tutte le variabili hanno tipi chiaramente definiti e non possono cambiare durante la chiamata ripetuta dello stesso codice. Sono quindi curioso di sapere se un giorno in PHP dichiareremo i tipi anche per le variabili: string $s = 'Ciao, questa è la conclusione della serie';