PHP 8 è stato ufficialmente rilasciato alla General Availability lo scorso 26 novembre 2020.
Questo nuovo importante aggiornamento porta tutta una serie di ottimizzazioni e potenti funzionalità al linguaggio e siamo felici di illustrarvi i cambiamenti più interessanti che ci permetteranno di scrivere codice migliore e costruire applicazioni più potenti.

PHP JIT (Just in Time Compiler)

La funzionalità più acclamata in arrivo con PHP 8 è il Compiler Just-in-time (JIT). Ma cos’è JIT?

La proposta dell’RFC descrive JIT come segue:

“PHP JIT è implementato come parte quasi indipendente di OPcache. Può essere abilitato/disabilitato al momento della compilazione di PHP e al run-time. Quando è abilitato, il codice nativo dei file PHP è memorizzato in una regione aggiuntiva della memoria condivisa di OPcache e gli op_array→opcodes[].handler(s) mantengono i puntatori sugli entry points del codice JIT.”

Allora, come siamo arrivati a JIT e qual è la differenza tra JIT e OPcache?

Per capire meglio cos’è JIT per PHP, diamo una rapida occhiata a come viene eseguito PHP dal codice sorgente fino al risultato finale.

L’esecuzione di PHP consiste in una procedura in 4 fasi:

  • Lexing/Tokenizing: per prima cosa, l’interprete legge il codice PHP e costruisce un set di token.
  • Parsing: l’interprete controlla se lo script corrisponde alle regole della sintassi e utilizza i token per costruire un Abstract Syntax Tree (AST), che è una rappresentazione gerarchica della struttura del codice sorgente.
  • Compilation: l’interprete attraversa l’albero e traduce i nodi AST in Zend opcode di basso livello, che sono identificatori numerici che determinano il tipo di istruzione eseguita dalla Zend VM.
  • Interpretation: gli opcode vengono interpretati ed eseguiti sulla Zend VM.

L’immagine che segue mostra una rappresentazione visuale della procedura di esecuzione di base di PHP.

Procedura di esecuzione di base di PHP

Allora, in che modo OPcache rende PHP più veloce? E cosa cambia nella procedura di esecuzione con JIT?

L’Estensione OPcache

PHP è un linguaggio interpretato. Ciò significa che, quando viene eseguito uno script PHP, l’interprete analizza, compila ed esegue il codice più e più volte ad ogni richiesta. Ciò può comportare uno spreco di risorse della CPU e di tempo aggiuntivo.

Qui entra in gioco l’estensione OPcache:

“OPcache migliora le prestazioni di PHP memorizzando il bytecode degli script precompilati nella memoria condivisa, eliminando così la necessità per PHP di caricare e analizzare gli script ad ogni richiesta.”

Con OPcache abilitato, l’interprete PHP attraversa la procedura a 4 fasi di cui sopra solo la prima volta che lo script viene eseguito. Dato che i bytecode PHP sono memorizzati nella memoria condivisa, sono immediatamente disponibili come rappresentazione intermedia di basso livello e possono essere eseguiti subito sulla Zend VM.

Procedura di esecuzione di PHP con OPcache abilitata

A partire da PHP 5.5, l’estensione Zend OPcache è disponibile di default e si può verificare se questa è configurata correttamente semplicemente invocando phpinfo() da uno script sul proprio server o controllando il file php.ini (si vedano le impostazioni di configurazione di OPcache).

Sezione Zend OPcache in una pagina phpinfo

Preloading

OPcache è stata recentemente migliorata con l’implementazione del preloading, una nuova funzionalità di OPcache aggiunta con PHP 7.4. Il preloading fornisce un modo per memorizzare un insieme specifico di script nella memoria di OPcache “prima che venga eseguito qualsiasi codice applicativo“, ma non porta un miglioramento tangibile delle prestazioni per le tipiche applicazioni basate sul web.

Con JIT, PHP fa un passo avanti.

JIT — Just in Time Compiler

Anche se gli opcode sono sotto forma di rappresentazione intermedia di basso livello, devono comunque essere compilati in codice macchina. JIT “non introduce alcuna forma aggiuntiva di rappresentazione intermedia (IR – Intermediate Representation)”, ma utilizza DynASM (Dynamic Assembler for code generation engines) per generare codice nativo direttamente dal byte-code PHP.

In breve, JIT traduce le parti calde del codice intermedio in codice macchina. Bypassando la compilazione, è in grado di apportare notevoli miglioramenti nelle prestazioni e nell’utilizzo della memoria.

Zeev Surasky, co-autore della proposta di PHP JIT, mostra quanto sarebbero più veloci i calcoli con JIT:

Ma JIT può migliorare davvero le prestazioni di WordPress?

JIT per le Live Web Apps

Secondo l’RFC di JIT, l’implementazione del compilatore just in time dovrebbe migliorare le prestazioni di PHP. Ma sperimenteremo davvero tali miglioramenti in applicazioni reali come WordPress?

I primi test mostrano che JIT renderebbe molto più veloci i carichi di lavoro ad alta intensità di CPU, tuttavia, avverte l’RFC:

“… come i precedenti tentativi – attualmente non sembra migliorare significativamente le applicazioni reali come WordPress (con opcache.jit=1235 326 req/sec vs 315 req/sec).

Si prevede uno sforzo ulteriore per migliorare JIT per le applicazioni real-life, utilizzando profilazione e ottimizzazioni speculative”.

Con JIT abilitato, il codice non verrebbe eseguito dalla Zend VM, ma dalla CPU stessa, e questo migliorerebbe la velocità di calcolo. Le applicazioni web come WordPress si basano anche su altri fattori come TTFB, ottimizzazione del database, richieste HTTP, ecc.

Contributo relativo di JIT alla performance di PHP (origine immagine: PHP 8.0 Announcement Addendum)

Quindi, per quel che riguarda WordPress e applicazioni simili, non dovremmo aspettarci un grande incremento della velocità di esecuzione di PHP. Tuttavia, JIT potrebbe portare diversi benefici agli sviluppatori.

Secondo Nikita Popov:

“I vantaggi del compilatore JIT sono grossomodo (e come già delineato nell’RFC):

  • Prestazioni significativamente migliori per il codice numerico.
  • Prestazioni leggermente migliori per il “tipico” codice di applicazioni web PHP.
  • La possibilità di spostare più codice da C a PHP, perché PHP sarà ora sufficientemente veloce”.

Così, anche se JIT difficilmente porterà enormi miglioramenti alle prestazioni di WordPress, porterà PHP al livello successivo, permettendo di scrivere funzioni direttamente nel linguaggio.

Il lato negativo, tuttavia, sarebbe la maggiore complessità che potrebbe portare ad un aumento dei costi di manutenzione, stabilità e debugging. Secondo Dmitry Stogov:

“JIT è estremamente semplice, ma in ogni caso aumenta il livello generale di complessità di PHP, comporterà rischi di nuovi tipi di bug e costi di sviluppo e manutenzione”.

La proposta di includere JIT in PHP 8 è passata con 50 voti favorevoli e 2 contrari.

PHP 8: Miglioramenti e Nuove Funzionalità

Oltre a JIT, con il rilascio di PHP 8 possiamo aspettarci molte altre funzionalità e miglioramenti. L’elenco che segue è la nostra selezione delle prossime aggiunte e modifiche che dovrebbero rendere PHP più affidabile ed efficiente.

Constructor Property Promotion

Come risultato di una discussione in corso su come migliorare l’ergonomia degli oggetti in PHP, la Constructor Property Promotion RFC propone una nuova e più concisa sintassi che semplifica la dichiarazione delle proprietà, rendendola più breve e meno ridondante.

Questa proposta si riferisce solo ai promoted parameters, cioè quei parametri di metodo preceduti dalle visibility keyword publicprotected e private.

Attualmente, tutte le proprietà devono essere ripetute più volte (almeno quattro volte) prima di poter essere utilizzate con gli oggetti. Si consideri il seguente esempio riportato sull’RFC:

class Point {
    public int $x;
    public int $y;
    public int $z;

    public function __construct(
        int $x = 0,
        int $y = 0,
        int $z = 0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

Secondo Nikita Popov, l’autore della RFC, dobbiamo scrivere il nome della proprietà almeno quattro volte in tre posti diversi: la dichiarazione della proprietà, i parametri del costruttore e l’assegnazione della proprietà. Questa sintassi non è particolarmente usabile, soprattutto nelle classi con un buon numero di proprietà e nomi più descrittivi.

Questa RFC propone di unire il costruttore e la definizione dei parametri. Così, a partire da PHP 8, abbiamo un modo più usabile di dichiarare i parametri e il codice visto sopra può cambiare come mostrato di seguito:

class Point {
    public function __construct(
        public int $x = 0,
        public int $y = 0,
        public int $z = 0,
    ) {}
}

E questo è tutto. Così abbiamo un nuovo modo di promuovere le proprietà che è più breve, più leggibile e meno soggetto a errori. Secondo Nikita:

È una semplice trasformazione sintattica che stiamo facendo. Ma questo riduce la quantità di codice boilerplate che si deve scrivere, in particolare per gli oggetti di valore…

La dichiarazione di proprietà viene trasformata come se avessimo dichiarato esplicitamente tali proprietà e possiamo utilizzare la Reflection API per effettuare un’analisi introspettiva delle definizioni delle proprietà prima dell’esecuzione (si veda Desugaring):

La Reflection (e altri meccanismi di introspezione) osserverà lo stato dopo il desugaring. Ciò significa che le proprietà promosse appariranno allo stesso modo delle proprietà dichiarate esplicitamente, e gli argomenti del costruttore promosso appariranno come argomenti ordinari del costruttore.

// before desugaring
class Point {
    public function __construct(public int $x = 0) {}
}

// after desugaring
class Point {
    public int $x;

    public function __construct(int $x = 0) {
        $this->x = $x;
    }
}

Ereditarietà

Non abbiamo alcuna limitazione nell’utilizzo dell’ereditarietà con la promozione dei parametri. In ogni caso, non c’è una relazione particolare tra i costruttori di classi genitori e classi figlie. Secondo Nikita:

Di solito, diciamo che i metodi devono sempre essere compatibili con il metodo della classe genitore. […] ma questa regola non vale per il costruttore. Quindi il costruttore appartiene realmente ad una singola classe, e non deve esserci in alcun modo compatibilità tra i costruttori della classe genitore e della classe figlia.

Ecco un esempio:

class Test {
    public function __construct(
        public int $x = 0
    ) {}
}

class Child extends Test {
    public function __construct(
        $x, 
        public int $y = 0,
        public int $z = 0,
    ) {
        parent::__construct($x);
    }
}

Cosa Non È Consentito con la Promozione delle Proprietà

Le proprietà promosse sono consentite nei costruttori non astratti e nei trait, ma ci sono diverse limitazioni che vale la pena menzionare.

Costruttori Astratti

Le proprietà promosse non sono ammesse nelle classi astratte e nelle interfacce:

abstract class Test {
    // Error: Abstract constructor.
    abstract public function __construct(private $x);
}
 
interface Test {
    // Error: Abstract constructor.
    public function __construct(private $x);
}
Nullability

Uno dei vincoli maggiori è legato alla nullability. In precedenza, quando si utilizzava un tipo che non era esplicitamente nullable, ma con un valore predefinito nullo, il tipo era implicitamente nullable. Ma con i property type, non abbiamo questo comportamento implicito perché i parametri promossi richiedono una dichiarazione di proprietà, e il tipo nullable deve essere dichiarato esplicitamente. Si veda l’esempio seguente riportato nella RFC:

class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}

    // Correct: Make the type explicitly nullable instead
    public function __construct(public ?Type $prop = null) {}
}
Tipo Callable

Dato che callable non è un tipo supportato per le proprietà, non ci è permesso utilizzare il tipo callable nella promozione dei parametri:

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}
La Keyword var Non È Ammessa

Solo una visibility keyword può essere utilizzata con i parametri promossi, quindi non è consentito dichiarare le proprietà del costruttore con la parola chiave var (si veda il seguente esempio della RFC):

class Test {
    // Error: "var" keyword is not supported.
    public function __construct(var $prop) {}
}
Non Sono Ammesse Duplicazioni

Possiamo combinare proprietà promosse e proprietà esplicite nella stessa classe, ma le proprietà non possono essere dichiarate due volte:

class Test {
    public string $prop;
    public int $explicitProp;

    // Correct
    public function __construct(public int $promotedProp, int $arg) {
        $this->explicitProp = $arg;
    }

    // Error: Redeclaration of property.
    public function __construct(public string $prop) {}
}
I Parametri Variadici Non Sono Ammessi

Il motivo è che il tipo dichiarato è diverso dal parametro variadico, che è in realtà un array:

class Test {
    // Error: Variadic parameter.
    public function __construct(public string ...$strings) {}
}

Ulteriori Letture

Per una discussione approfondita della Costructor Property Promotion, ascoltate questa intervista a Nikita Popov. Per una panoramica approfondita dell’ergonomia degli oggetti in PHP, si veda questo articolo e la successiva intervista a Larry Garfield.

Validazione per i Metodi Astratti dei Trait

I Trait sono definiti come “un meccanismo per il riutilizzo del codice in linguaggi a single inheritance come PHP”. Sono utilizzati per dichiarare metodi che possono essere usati in più classi.

Un trait può anche contenere metodi astratti. Questi metodi si limitano a dichiarare la firma del metodo, ma l’implementazione deve essere fatta all’interno della classe che utilizza il trait.

Secondo il manuale PHP,

“I trait supportano l’utilizzo di metodi astratti per imporre requisiti alla classe che li espone”.

Ciò significa anche che le firme dei metodi devono corrispondere. In altre parole, il tipo e il numero di argomenti richiesti devono essere gli stessi.

Ad ogni modo, secondo Nikita Popov, autore dell’RFC, la convalida della firma è attualmente applicata solo in modo irregolare:

Il seguente esempio di Nikita si riferisce al primo caso (firma non forzata):

trait T {
    abstract public function test(int $x);
}
 
class C {
    use T;

    // Allowed, but shouldn't be due to invalid type.
    public function test(string $x) {}
}

Detto questo, questa RFC propone di emettere sempre un errore fatale se il metodo che implementa non è compatibile con il metodo astratto del trait, indipendentemente dalla sua origine:

Fatal error: Declaration of C::test(string $x) must be compatible with T::test(int $x) in /path/to/your/test.php on line 10

Questa RFC è stato approvato all’unanimità.

Firme di Metodi Incompatibili

In PHP, gli errori di ereditarietà dovuti a firme di metodi incompatibili generano o un errore fatale o un warning a seconda di ciò che causa l’errore.

Se una classe sta implementando un’interfaccia, le firme dei metodi incompatibili generano un errore fatale. Secondo la documentazione delle Object Interface:

“La classe che implementa l’interfaccia deve utilizzare una firma di metodo compatibile con l’LSP (Principio di Sostituzione di Liskov). Se non lo fa, si otterrà un errore fatale.”

Ecco un esempio di errore di ereditarietà con un’interfaccia:

interface I {
    public function method(array $a);
}
class C implements I {
    public function method(int $a) {}
}

In PHP 7.4, il codice di cui sopra avrebbe generato il seguente errore:

Fatal error: Declaration of C::method(int $a) must be compatible with I::method(array $a) in /path/to/your/test.php on line 7

Una funzione in una child class con una firma incompatibile genererebbe un warning. Si consideri il seguente codice tratto dall’RFC:

class C1 {
    public function method(array $a) {}
}
class C2 extends C1 {
    public function method(int $a) {}
}

In PHP 7.4, il codice qui sopra non farebbe altro che generare un avviso:

Warning: Declaration of C2::method(int $a) should be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Ora, questa RFC propone di emettere sempre un errore fatale per le firme di metodo incompatibili. Con PHP 8, il codice che abbiamo visto in precedenza visualizzerebbe quanto segue:

Fatal error: Declaration of C2::method(int $a) must be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

Array che Iniziano con un Indice Negativo

In PHP, se un array inizia con un indice negativo (start_index < 0), gli indici successivi partiranno da 0 (maggiori informazioni su questo punto nella documentazione di array_fill). Si consideri il seguente esempio:

$a = array_fill(-5, 4, true);
var_dump($a);

In PHP 7.4 il risultato sarebbe il seguente:

array(4) {
    [-5]=>
    bool(true)
    [0]=>
    bool(true)
    [1]=>
    bool(true)
    [2]=>
    bool(true)
}

Ora, questa RFC propone di cambiare le cose in modo che il secondo indice sia start_index + 1, qualunque sia il valore di start_index.

In PHP 8, il codice di cui sopra risulterebbe nel seguente array:

array(4) {
    [-5]=>
    bool(true)
    [-4]=>
    bool(true)
    [-3]=>
    bool(true)
    [-2]=>
    bool(true)
}

Dunque, con PHP 8, gli array che iniziano con un indice negativo cambiano il loro comportamento. Per saperne di più sulle retro-incompatibilità si legga la RFC.

Union Types 2.0

Gli Union Types accettano valori che possono essere di diversi tipi. Attualmente, PHP non fornisce supporto per gli Union Types, con l’eccezione della sintassi del tipo ?Type e del tipo speciale iterabile.

Prima di PHP 8, gli Union Types potevano essere specificati solo nelle annotazioni phpdoc, come mostrato nell’esempio seguente tratto dall’RFC:

class Number {
    /**
     * @var int|float $number
     */
    private $number;

    /**
     * @param int|float $number
     */
    public function setNumber($number) {
        $this->number = $number;
    }

    /**
     * @return int|float
     */
    public function getNumber() {
        return $this->number;
    }
}

La RFC Union Types 2.0 propone di aggiungere il supporto per gli Union Type nelle firme delle funzioni, in modo da non fare più affidamento sulla documentazione in linea, ma definisce invece gli Union Types con la sintassi T1|T2|....:

class Number {
    private int|float $number;

    public function setNumber(int|float $number): void {
        $this->number = $number;
    }

    public function getNumber(): int|float {
        return $this->number;
    }
}

Come spiegato da Nikita Popov nella RFC,

“Il supporto degli Union Types nel linguaggio ci permette di spostare più informazioni sui tipi da phpdoc delle firme delle funzioni, con i soliti vantaggi che questo comporta:

  • I tipi sono effettivamente applicati, quindi gli errori possono essere colti in anticipo.
  • Dato che sono imposte, è meno probabile che le informazioni sui tipi diventino obsolete o che non siano aggiornate.
  • I tipi sono controllati durante l’ereditarietà, facendo rispettare il Principio di Sostituzione di Liskov.
  • I tipi sono disponibili tramite Reflection.
  • La sintassi è molto meno da “boilerplate” rispetto a phpdoc”.

Gli Union Types supportano tutti i tipi disponibili, con alcune limitazioni:

  • Il tipo void non può essere parte di un’unione, in quanto void significa che una funzione non restituisce alcun valore.
  • Il tipo null è supportato solo negli Union Types, ma il suo utilizzo come tipo standalone non è consentito.
  • Anche la notazione di tipo nullable (?T) è permessa, che significa T|null, ma non è permesso includere la notazione ?T negli Union Types (?T1|T2 non è permessa e dovremmo usare invece T1|T2|null).
  • Dato che molte funzioni (cioè strpos()strstr()substr(), ecc.) includono false tra i possibili tipi di ritorno, è supportato anche lo pseudotipo false.

Potete leggere di più sugli Union Types V2 nell’RFC.

Errori di Tipo Coerenti per le Funzioni Interne

Quando si passa un parametro di tipo illegale, le funzioni interne e quelle definite dall’utente si comportano in modo diverso.

Le funzioni definite dall’utente generano un TypeError, ma le funzioni interne si comportano in diversi modi, in base a diverse condizioni. In ogni caso, il comportamento tipico è quello di emettere un avvertimento e restituire null. Si consideri il seguente esempio in PHP 7.4:

var_dump(strlen(new stdClass));

Questo risulterebbe nel seguente avviso:

Warning: strlen() expects parameter 1 to be string, object given in /path/to/your/test.php on line 4
NULL

Il comportamento sarebbe diverso se strict_types è abilitato, o se le informazioni sugli argomenti specificano i tipi. In tali scenari, l’errore del tipo viene rilevato e risulta in un TypeError.

Questa situazione comporterebbe ad una serie di problemi ben illustrati nella sezione Issues della RFC.

Per rimuovere queste incongruenze, questa RFC propone far sì che le API interne di analisi dei parametri generino sempre un ThrowError in caso mancata corrispondenza dei tipi dei parametri.

In PHP 8, il codice sopra riportato genera il seguente errore:

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, object given in /path/to/your/test.php:4
Stack trace:
#0 {main}
  thrown in /path/to/your/test.php on line 4

throw Come Espressione

In PHP, throw è una dichiarazione, quindi non è possibile utilizzarlo in situazioni in cui è consentita solo un’espressione.

Questa RFC propone di convertire l’istruzione throw in un’espressione in modo che possa essere utilizzata in qualsiasi contesto in cui sono consentite le espressioni. Ad esempio, arrow functionoperatore null coalesceoperatori ternari ed elvis, ecc.

Si vedano i seguenti esempi tratti dall’RFC:

$callable = fn() => throw new Exception();

// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();
 
// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();

Weak Maps

Una weak map è una collezione di dati (oggetti) in cui le chiavi sono referenziate debolmente, il che significa che non viene impedito loro di eliminate (garbage collected).

PHP 7.4 ha aggiunto il supporto per le weak reference come modo per mantenere un riferimento ad un oggetto che non impedisce all’oggetto stesso di essere distrutto. Come sottolineato da Nikita Popov,

“Le weak reference grezze da soli sono di limitata utilità e le weak map nella pratica sono utilizzate molto più spesso. Non è possibile implementare un’efficiente weak map al top delle weak reference di PHP perché non è prevista la possibilità di registrare una callback di distruzione”.

Ecco perché questa RFC introduce una classe WeakMap per creare oggetti da utilizzare come chiavi di weak map che possono essere distrutte e rimosse dalla weak map nel caso in cui non ci siano ulteriori riferimenti all’oggetto key.

Nei processi di lunga durata, ciò eviterebbe perdite di memoria e migliorerebbe le prestazioni. Si veda questo esempio tratto dall’RFC:

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

Con PHP 8, il codice di cui sopra produrrebbe il seguente risultato (vedere il codice in azione qui):

object(WeakMap)#1 (1) {
    [0]=>
    array(2) {
        ["key"]=>
        object(stdClass)#2 (0) {
        }
        ["value"]=>
        int(42)
    }
}

Se si deregistra l’oggetto, la chiave viene automaticamente rimossa dalla weak map:

unset($obj);
var_dump($map);

Adesso il risultato sarebbe il seguente:

object(WeakMap)#1 (0) {
}

Per una descrizione più dettagliata delle Weak map, si legga la RFC. La proposta è stata approvata all’unanimità.

Virgola Finale negli Elenchi di Parametri

Le virgole finali sono virgole aggiunte ad elenchi di elementi in diversi contesti. PHP 7.2 ha introdotto le virgole finali nella sintassi delle liste, PHP 7.3 ha introdotto le virgole finali nelle chiamate di funzione.

PHP 8 introduce adesso le virgole finali nelle liste di parametri in funzioni, metodi e closure, come mostrato nell’esempio che segue:

class Foo {
    public function __construct(
        string $x,
        int $y,
        float $z, // trailing comma
    ) {
        // do something
    }
}

La proposta è passata con 58 voti favorevoli e 1 contrario.

Sintassi ::class Ammessa sugli Oggetti

Per ottenere il nome di una classe, possiamo utilizzare la sintassi Foo\Bar::classQuesta RFC propone di estendere la stessa sintassi agli oggetti, in modo che sia ora possibile recuperare il nome della classe di un dato oggetto, come mostrato nel seguente esempio:

$object = new stdClass;
var_dump($object::class); // "stdClass"
 
$object = null;
var_dump($object::class); // TypeError

A partire da PHP 8, $object::class fornisce lo stesso risultato di get_class($object). Se $object non è un oggetto, allora viene generata un’eccezione TypeError.

La proposta è stata approvata all’unanimità.

Attributi v2

Gli attributi, noti anche come annotazioni, sono una forma di metadati strutturati che possono essere utilizzati per specificare le proprietà di oggetti, elementi o file.

Fino a PHP 7.4, i doc-comment erano l’unico modo per aggiungere metadati alle dichiarazioni di classi, funzioni, ecc. Ora, la Attributes v2 RFC introduce gli attributi in PHP, definendoli come una forma di metadati sintattici strutturati che possono essere aggiunti alle dichiarazioni di classi, proprietà, funzioni, metodi, parametri e costanti.

Gli attributi vengono aggiunti prima delle dichiarazioni a cui si riferiscono. Si vedano i seguenti esempi tratti dall’RFC:

<<ExampleAttribute>>
class Foo
{
    <<ExampleAttribute>>
    public const FOO = 'foo';

    <<ExampleAttribute>>
    public $x;

    <<ExampleAttribute>>
    public function foo(<<ExampleAttribute>> $bar) { }
}

$object = new <<ExampleAttribute>> class () { };

<<ExampleAttribute>>
function f1() { }

$f2 = <<ExampleAttribute>> function () { };

$f3 = <<ExampleAttribute>> fn () => 1;

Gli attributi possono essere aggiunti sia prima che dopo un commento del doc-block:

<<ExampleAttribute>>
/** docblock */
<<AnotherExampleAttribute>>
function foo() {}

Ogni dichiarazione può avere uno o più attributi e ogni attributo può avere uno o più valori associati:

<<WithoutArgument>>
<<SingleArgument(0)>>
<<FewArguments('Hello', 'World')>>
function foo() {}

Si legga la RFC per una descrizione più approfondita degli attributi in PHP, dei casi d’uso e della sintassi alternativa.

Argomenti Denominati

Gli argomenti denominati (“named arguments”) forniscono un nuovo modo di passare gli argomenti ad una funzione in PHP:

Gli argomenti denominati permettono di passare argomenti ad una funzione in base al nome del parametro, invece che che alla posizione del parametro.

Possiamo passare argomenti denominati ad una funzione semplicemente aggiungendo il nome del parametro prima del suo valore:

callFunction(name: $value);

Possiamo anche utilizzare parole chiave riservate, come mostrato nell’esempio qui sotto:

callFunction(array: $value);

Ma non è possibile passare un nome di parametro in modo dinamico. Il parametro deve essere un identificatore e la seguente sintassi non è ammessa:

callFunction($name: $value);

Secondo Nikita Popov, l’autore di questo RFC, i named arguments offrono diversi vantaggi.

Prima di tutto, gli argomenti denominati ci aiuteranno a scrivere codice più comprensibile perché il loro significato è auto-documentato. L’esempio che segue dell’RFC si spiega da solo:

array_fill(start_index: 0, num: 100, value: 50);

Gli argomenti denominati sono indipendenti dall’ordine. Ciò significa che non siamo costretti a passare argomenti a una funzione nello stesso ordine della firma della funzione:

array_fill(value: 50, num: 100, start_index: 0);

E’ anche possibile combinare argomenti denominati con argomenti posizionali:

htmlspecialchars($string, double_encode: false);

Un altro grande vantaggio degli argomenti denominati è che permettono di specificare solo quegli argomenti che vogliamo effettivamente cambiare e non dobbiamo specificare gli argomenti predefiniti se non vogliamo sovrascriverne i valori. Si veda l’esempio seguente tratto dalla RFC:

htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode: false);

Gli argomenti denominati possono essere utilizzati con gli attributi PHP, come mostrato nell’esempio seguente tratto dall’RFC:

<<MyAttribute('A', b: 'B')>>
class Test {}

Tuttavia, non è consentito passare argomenti posizionali dopo gli argomenti denominati e comporta un errore di compilazione. Lo stesso accade quando si passa lo stesso nome di parametro due volte.

Gli argomenti denominati sono particolarmente utili con le dichiarazioni di classe perché i costruttori di solito hanno un gran numero di parametri e gli argomenti denominati forniscono un modo più “ergonomico” per dichiarare una classe.

Per un’analisi più approfondita dei Named Argument, con vincoli, incompatibilità e numerosi esempi, si veda la Named Arguments RFC.

Operatore Nullsafe

Questa RFC introduce l’operatore nullsafe $-> con valutazione a corto circuito completa.

Nella valutazione a corto circuito, il secondo operatore viene valutato solo se il primo operatore non viene valutato null. Se un operatore in una catena viene valutato null, l’esecuzione dell’intera catena si ferma e viene valutata null.

Si consideri il seguente esempio tratto dalla RFC:

$foo = $a?->b();

Se $a è null, il metodo b() non viene invocato e $foo è impostato a null.

Si veda la Nullsafe Operator RFC per altri esempi, eccezioni e futuro ambito di applicazione.

Comparazioni più Coerenti di Stringhe e Numeri

Nelle versioni precedenti di PHP, quando si effettuava un confronto non rigoroso tra stringhe e numeri, PHP prima converte la stringa a numero, poi esegue il confronto tra numeri interi o float. Anche se questo comportamento è abbastanza utile in diverse situazioni, può produrre risultati sbagliati che possono anche portare a bug e/o problemi di sicurezza.

Si consideri il seguente esempio dell’RFC:

$validValues = ["foo", "bar", "baz"];
$value = 0;
var_dump(in_array($value, $validValues));
// bool(true)

PHP 8 introduce Comparazioni più coerenti di stringhe e numeri, mirando a rendere più ragionevoli i confronti tra stringhe e numeri. Nelle parole di Nikita Popov,

Questa RFC intende dare ai confronti tra stringhe di numeri un comportamento più ragionevole: quando si confronta con una stringa numerica, effettua un confronto numerico (come adesso). Altrimenti, converte il numero in stringa ed effettua un confronto tra stringhe.

La seguente tabella mette a confronto il comportamento del confronto tra stringhe e numeri nelle versioni precedenti di PHP e in PHP 8:

Comparison    | Before | After
------------------------------
 0 == "0"     | true   | true
 0 == "0.0"   | true   | true
 0 == "foo"   | true   | false
 0 == ""      | true   | false
42 == "   42" | true   | true
42 == "42foo" | true   | false

Per un’analisi più approfondita delle molte implicazioni di questa modifica e vedere come cambiano i confronti da stringa a numero in PHP 8, si legga la RFC di Nikita Popov.

Stringhe Numeriche più Coerenti

In PHP le stringhe contenenti numeri ricadono in tre categorie:

  • Stringhe numeriche: stringhe contenenti un numero eventualmente preceduto da spazi bianchi.
  • Stringhe che iniziano con un numero: stringhe i cui caratteri iniziali sono stringhe numeriche e i caratteri finali sono non numerici.
  • Stringhe non numeriche: le stringhe che non rientrano in nessuna delle precedenti categorie.

Le stringhe numeriche e le stringhe che iniziano con un numero sono trattate in modo diverso a seconda dell’operazione eseguita. Ad esempio:

  • Le conversioni esplicite da stringa a numero (es. conversione di tipi tramite (int) e (float)) convertono in numeri le stringhe numeriche e che iniziano con numeri. Convertire esplicitamente una stringa non numerica in un numero produce 0.
  • Le conversioni implicite da stringa a numero (es. assenza di dichiarazione strict_type) portano a risultati diversi per le stringhe numeriche e non numeriche. Le conversioni da stringa non numerica a numero generano un TypeError.
  • is_numeric() restituisce true solo per le stringhe numeriche.

Anche gli offset delle stringhe, le operazioni aritmetiche, le operazioni di incremento e decremento, i confronti da stringa a stringa e le operazioni bitwise portano a risultati diversi.

Questa RFC propone di:

Unificare le varie modalità di stringa numerica in un unico concetto: Caratteri numerici solo con spazi bianchi sia iniziali che finali consentiti. Qualsiasi altro tipo di stringa non è numerico e genera TypeError se utilizzato in un contesto numerico.

Ciò significa che tutte le stringhe che attualmente emettono E_NOTICE “A non well formed numeric value encountered” saranno riclassificate in E_WARNING “A non-numeric value encountered”, a meno che la stringa che inizia con un numero non contenga solo spazi bianchi finali. E i vari casi che attualmente generano un E_WARNING saranno promossi a TypeErrors.

Per un’analisi più approfondita delle stringhe numeriche in PHP 8, con esempi di codice, eccezioni e problemi di retrocompatibilità, si veda la RFC.

Espressione Match v2

La nuova espressione match è molto simile a switch ma con una semantica più sicura e permettendo di restituire valori.

Per capire meglio la differenza tra le due strutture di controllo, si consideri il seguente esempio di switch tratto dalla RFC:

switch (1) {
 case 0:
 $result = 'Foo';
 break;
 case 1:
 $result = 'Bar';
 break;
 case 2:
 $result = 'Baz';
 break;
}
 
echo $result;
//> Bar

Ora possiamo ottenere lo stesso risultato del codice di cui sopra con la seguente espressione match:

echo match (1) {
 0 => 'Foo',
 1 => 'Bar',
 2 => 'Baz',
};
//> Bar

Un grande vantaggio offerto dall’espressione match è che, mentre switch effettua un confronto debole (==) che potrebbe portare a risultati inaspettati, con match il confronto è un controllo di identità (===).

L’espressione match può anche contenere espressioni multiple separate da virgole, consentendo una sintassi più concisa (fonte):

$result = match ($x) {
 // This match arm:
 $a, $b, $c => 5,
 // Is equivalent to these three match arms:
 $a => 5,
 $b => 5,
 $c => 5,
};

Per ulteriori esempi e casi d’uso, si veda la Match expression v2 RFC and the documentazione PHP.

Controlli più Stringenti per gli Operatori Aritmetici e Bitwise

Nelle versioni precedenti di PHP, applicare operatori aritmetici e bitwise ad un array, una risorsa o un oggetto non sovraccarico era consentito. Tuttavia, il comportamento era a volte incoerente.

In questa RFC, Nikita Popov mostra quanto sia irragionevole questo comportamento con un semplice esempio:

var_dump([] % [42]);
// int(0)

Nikita spiega come l’applicazione di un operatore aritmetico o bitwise agli array, alle risorse o agli oggetti non sovraccarichi portava a risultati diversi:

Operatori +, -, *, /, **:

  • Lancia l’eccezione di errore sull’array operando. (Escluso + se entrambi gli operandi sono array).
  • Converte silenziosamente un operando della risorsa nell’ID della risorsa come intero.
  • Converte un operando oggetto in un operando intero, mentre genera un avviso.

Operatori %, <<, >>, &, |, ^:

  • Converte silenziosamente un array operando in un numero intero zero se vuoto o in un numero intero uno se non vuoto.
  • Converte silenziosamente una risorsa operando all’ID della risorsa come numero intero.
  • Converte un oggetto operando in un intero, mentre genera un avviso.

Operatore ~:

  • Genera un’eccezione di errore per gli operandi array, risorse e oggetti.

Operatori ++ and –:

  • Non fa nulla se l’operando è un array, una risorsa o un oggetto.

Con PHP 8 le cose cambiano e il comportamento è lo stesso per tutti gli operatori aritmetici e bitwise:

Genera un’eccezione TypeError per operandi array, risorse e oggetti.

Nuove Funzioni di PHP

PHP 8 introduce nel linguaggio diverse nuove funzioni:

str_contains

Prima di PHP 8, strstr e strpos erano le opzioni tipiche che gli sviluppatori potevano per cercare un needle all’interno di una data stringa. Il problema è che entrambe le funzioni non sono molto intuitive e il loro utilizzo può confondere gli sviluppatori PHP alle prime armi. Si consideri il seguente esempio:

$mystring = 'Managed WordPress Hosting';
$findme = 'WordPress';
$pos = strpos($mystring, $findme);

if ($pos !== false) {
    echo "The string has been found";
} else {
    echo "String not found";
}

In questo esempio abbiamo utilizzato l’operatore di confronto !==, che controlla anche se due valori sono dello stesso tipo. Questo ci evita di ricevere un errore se la posizione del needle è 0:

“Questa funzione può restituire il valore booleano FALSE, ma può anche restituire un valore non booleano che corrisponde a FALSE. […] Si utilizzi l’operatore ==== per testare il valore di ritorno di questa funzione”.

Inoltre, diversi framework forniscono funzioni helper per la ricerca di un valore all’interno di una determinata stringa (si veda come esempio la documentazione degli Helper di Laravel).

Ora, questa RFC propone l’introduzione di una nuova funzione che permette di cercare all’interno di una stringa: str_contains.

str_contains ( string $haystack , string $needle ) : bool

Il suo utilizzo è piuttosto semplice. str_contains verifica se $needle si trova in $haystack e restituisce true o false di conseguenza.

Quindi, grazie a str_contains, possiamo scrivere il seguente codice:

$mystring = 'Managed WordPress Hosting';
$findme   = 'WordPress';

if (str_contains($mystring, $findme)) {
    echo "The string has been found";
} else {
    echo "String not found";
}

Che è più leggibile e meno soggetto ad errori (si può vedere questo codice in azione qui).
Al momento in cui scriviamo, str_contains è case-sensitive, ma questa caratteristica potrebbe cambiare in futuro.

La proposta di str_contains è stata approvata con 43 voti favorevoli e 9 contrari.

str_starts_with() e str_ends_with()

Oltre alla funzione str_contains, due nuove funzioni permettono di cercare un needle (ago) all’interno di una determinata stringa: str_starts_with e str_ends_with.

Queste nuove funzioni verificano se una data stringa inizia o finisce con un’altra stringa:

str_starts_with (string $haystack , string $needle) : bool
str_ends_with (string $haystack , string $needle) : bool

Entrambe le funzioni restituiscono false se $needle è più lungo di $haystack.

Secondo Will Hudgins, l’autore di questa RFC,

“La funzionalità di str_starts_with e str_ends_with è così normalmente necessaria che è supportata da molti dei principali framework PHP, tra cui SymfonyLaravelYiiFuelPHP e Phalcon“.

Grazie a queste funzioni, da ora in poi ora potremo evitare di utilizzare funzioni non ottimali e meno intuitive come substrstrpos. Entrambe le funzioni sono case sensitive:

$str = "WordPress";
if (str_starts_with($str, "Word")) echo "Found!";

if (str_starts_with($str, "word")) echo "Not found!";

Potete vedere questo codice in azione qui.

Questa RFC è stata approvata con 51 voti favorevoli e 4 contrari.

get_debug_type

get_debug_type è una nuova funzione PHP che restituisce il tipo di una variabile. La nuova funzione lavora in modo molto simile alla funzione gettype, ma get_debug_type restituisce i nomi nativi dei tipi e risolve i nomi delle classi.

Si tratta di un buon miglioramento per il linguaggio, perché gettype() non è utile per la verifica dei tipi.

La RFC fornisce due utili esempi per comprendere meglio la differenza tra la nuova funzione get_debug_type() e gettype(). Il primo esempio mostra gettype al lavoro:

$bar = [1,2,3];

if (!($bar instanceof Foo)) { 
    throw new TypeError('Expected ' . Foo::class . ', got ' . (is_object($bar) ? get_class($bar) : gettype($bar)));
}

Da PHP 8 in poi, potremo invece utilizzare get_debug_type:

if (!($bar instanceof Foo)) { 
    throw new TypeError('Expected ' . Foo::class . ' got ' . get_debug_type($bar));
}

La seguente tabella mostra i valori restituiti da get_debug_type e gettype:

Valore gettype() get_debug_type()
1 integer int
0.1 double float
true boolean bool
false boolean bool
null NULL null
“WordPress” string string
[1,2,3] array array
Una classe con nome “Foo\Bar” object Foo\Bar
Una classe anonima object class@anonymous

Altre RFC

Ecco un breve elenco di altre novità approvate che fanno parte di PHP 8:

  1. Interfaccia Stringablequesta RFC introduce un’interfaccia stringable che viene aggiunta automaticamente alle classi che implementano il metodo __to String(). Lo scopo principale è quello di utilizzare lo union type string|Stringable.
  2. Nuove API DOM Living Standard in ext/domquesta RFC si propone di implementare l’attuale DOM Living Standard nell’estensione PHP DOM, introducendo nuove interfacce e proprietà pubbliche.
  3. Tipo di ritorno static: PHP 8 introduce il tipo static come tipo di ritorno accanto ai tipi self e parent.
  4. Variable Syntax Tweaksquesta RFC risolve alcune incongruenze residue nella sintassi delle variabili di PHP.

Che corsa! In questo post abbiamo trattato tutte le principali ottimizzazioni e le funzionalità portate da PHP 8. Il più atteso è sicuramente il compilatore Just in Time, ma c’è molto di più in PHP 8.