Vai al contenuto

Union Type in PHP 8.0

PHP ha fatto molta strada con i tipi. Ora abbiamo tipi scalari, tipi restituiti, tipi nullable e persino tipi di proprietà in PHP 7.4. Dalla versione PHP 8.0 viene fornito il supporto agli Union Type. 

Nelle versioni precedenti a PHP 8.0, era possibile dichiarare un solo tipo per proprietà, parametri e tipi restituiti. Da PHP 8.0, possiamo dichiarare più di un tipo per argomenti, tipi restituiti e proprietà di classe. Ad esempio, una dichiarazione di questo tipo, su PHP 8.0 è lecita:

class Example {
    private int|float $foo;
    public function squareAndAdd(float|int $bar): int|float {
        return $bar ** 2 + $foo;
    }
}

PHP si assicurerà che i parametri della funzione, i tipi restituiti e le proprietà della classe appartengano a uno dei tipi dichiarati nella definizione.

Type Union in PHP 8.0

Da PHP 8.0, possiamo dichiarare un numero qualsiasi di tipi arbitrari per proprietà, argomenti e tipi restituiti. Ci sono alcune eccezioni che non hanno senso da usare insieme, come ad esempio string|void. Ne vedremo qualcuno strada facendo…

Possiamo eliminare completamente i commenti PHPDoc @var, @param, e @return a favore dei tipi applicati nel codice stesso. Questo aiuterà a ripulire i commenti che magari erano messi lì semplicemente perché i tipi non potevano essere dichiarati prima nel codice.

I tipi nullable esistenti non vengono rimossi né deprecati. Possiamo continuare a usare ?string come scorciatoia per string|null.

Anche il tipo iterable non viene rimosso ed è funzionalmente uguale a array|Traversable.

Il tipo void non è consentito

PHP ha già il supporto per il tipo void. È consentito solo come tipo restituito perché l’utilizzo di void in qualsiasi altro luogo non avrebbe alcun senso. Ovviamente la funzione non deve restituire alcun valore o terminare con return; senza specificare un valore. Nelle Union Types di PHP 8.0, il tipo void non può essere combinato con nessun altro tipo. Ad esempio, qualcosa del genere non è ammessa:

function foo(): void|null {}

Tipo speciale false

Molte funzioni del Core di PHP e molte librerie legacy ritornano false per indicare un risultato negativo. Ad esempio, se si dispone di una funzione che carica l’account di un utente in base al suo ID, la funzione potrebbe ritornare false per indicare che l’utente non esiste. Per aiutare l’adozione di type union, è consentito l’uso di false.

function user_load(int $id): User|false { }

Lo snippet sopra imita la funzione di Drupal user_load(), dove restituisce un oggetto User se viene trovato l’account utente o false altrimenti. Questo tipo speciale false può essere molto utile in questa situazione. Esistono anche diverse funzioni principali di PHP che seguono lo stesso schema. Ad esempio, la funzione strpos() restituisce una int della posizione in cui sono stati trovati i caratteri corrispondenti o false se la stringa ricercata non viene trovata.

Alcune eccezioni:

  • false non può essere utilizzato come tipo autonomo. Ciò significa che una dichiarazione di tipo simile public false $foo non è consentita mentre viene usato il tipo bool. A partire da PHP 8.2 , è invece consentito l’utilizzo di false come tipo autonomo.
  • Se è utilizzato bool, allora false non può essere utilizzato nella stessa dichiarazione di tipo.

Tipi Nullable (?TYPE) e null non devono essere mischiati.

Sebbene ?string sia una scorciatoia per string|null, le due notazioni non devono essere mescolate. Se la tua dichiarazione di tipo ha più di un tipo e null, deve essere dichiarata come segue:

TYPE_1|TYPE_2|null

Non può essere dichiarato ?TYPE_1|TYPE_2 in quanto si tratterebbe di una dichiarazione ambigua.

Non sono consentiti tipi duplicati

Non puoi dichiarare int|intint|INT poiché essenzialmente è la stessa cosa. Non è nemmeno consentito qualcosa del tipo int|?int. Quest’ultimo darà un errore di sintassi mentre il primo genererà un errore di ridondanza:

Fatal error: Duplicate type ... is redundant in ... on line ...

I tipi ridondanti non sono consentiti

A proposito di ridondanza, i type union non consentono tipi di classe ridondanti. Tuttavia, è importante notare che ciò si applica solo ad alcuni tipi speciali speciali.

  • bool|false non è consentito perché false è un tipo di bool.
  • `object non può essere utilizzato con un nome di classe perché tutti gli oggetti di classe sono di tipo object.
  • iterable non può essere utilizzato con array o Traversable perché iterable è già un union  type array|Traversable.
  • I nomi delle classi possono essere usati anche se uno ne estende un altro.

Poiché le dichiarazioni degli union type vengono convalidate in fase di compilazione, non verranno generati errori se si utilizza una classe padre e una classe figlia in union perché ciò richiederebbe la risoluzione della gerarchia di classi.

Ecco alcuni esempi:

function foo(): bool|false {} // Fatal error: Duplicate type false is redundant in ... on line ... 
function foo(): DateTime|object {} // Fatal error: Type DateTime|object contains both object and a class type, which is redundant in ... on line ... 
function foo(): iterable|array {} // Fatal error: Type iterable|array contains both iterable and array, which is redundant in ... on line ...

Poiché la dichiarazione del tipo viene verificata solo in fase di compilazione, è valido quanto segue:

class A{} 
class B extends A{} 
function foo(): A|B {}

Varianza

L’ereditarietà in PHP applica il principio di sostituzione di Liskov (LSP) per assicurarsi che un’istanza della classe genitore possa essere sostituita con un’istanza della classe figlia. Naturalmente, PHP non può applicarlo in piena generalità: può rilevare casi decisamente incompatibili solo ispezionando le dichiarazioni dei membri della classe. Ad esempio, queste due classi sono chiaramente incompatibili:

class A {
   public function method(string $arg) {}
}
class B extends A {
   public function method(int $arg) {}
}

Un’istanza di A non può essere sostituita da B, perché A::method() accetterà  la stringa”foobar”, mentre B::method() non lo farà.

Per garantire che tutte le sottoclassi e le implementazioni dell’interfaccia non debbano modificare il comportamento del programma e continuare a rispettare il contratto, vengono applicate le seguenti regole

  • I parametri sono controvarianti: i tipi possono essere ampliati con un tipo super.
  • I tipi restituiti sono covarianti: i tipi possono essere limitati a un sottotipo.
  • Tipi di proprietà invarianti: i tipi non possono essere modificati in un tipo secondario o super.

Aggiunta o rimozione di tipi a un’unione

class A { 
    public function foo(string|int $foo): string|int {} 
} 

class B extends A { 
    public function foo(string|int|float $foo): string {} 
}

Nello script sopra, i tipi di parametro vengono ampliati (con l’aggiunta di float). Questo è consentito perché tutti i programmi che usano la classe B si aspettano che accettino tutti i tipi che accetta la classe A. La Classe B adempie ancora a questo “contratto”. Il tipo restituito in B::foo seppur limitato (manca infatti l’int), segue ancora LSP perché la classe B soddisfa il tipo restituito dichiarato in A::foo.

Se dovesse cambiare il tipo in modo che la classe B non soddisfi il contratto, verrà attivato un errore irreversibile:

class A {
    public function foo(string|int $foo): string|int {}
}
class B extends A {
    public function foo(string|float $foo): string|int {}
}
Fatal error: Declaration of B::foo(string|float $foo): string|int must be compatible with A::foo(string|int $foo): string|int in ... on line ...

Varianza dei singoli tipi in Union Type

Le stesse regole di varianza vengono seguite quando si modifica un union type dei singoli membri.

Sottoclassi

class ParentClass {} 
class ChildClass extends ParentClass{} 
class FooParent { 
    public function foo(string|ChildClass $a): string {} 
    public function bar(string $b): string:ParentClass {} 
} 
class FooChild extends FooParent{ 
    public function foo(string|ParentClass $a): string|ChildClass {} 
    public function bar(string $b): string:ChildClass {} 
}

Questo è lecito, perché nelle funzioni FooChild::foo  i parametri sono espansi. Tutto il codice esistente che funziona con la classe FooParent funzionerà perché ChildClass è un sottotipo di ParentClass con la stessa funzionalità.

Nella funzione FooChild::bar, il suo tipo restituito è ulteriormente limitato. Ciò vale anche perché FooChild::bar il quale adempie al suo contratto restituendo un sottotipo di stringChildClass che eredita ParentClass.

Nullable

class FooParent { 
    public function foo(string $a): int|null {} 
} 
class FooChild extends FooParent{ 
    public function foo(string|null $b): int|null {} 
}

I tipi restituiti possono perdere il tipo nullable e i parametri possono accettare null.

bool e false

In un tipo Union, PHP considera false un sottotipo per bool. Ciò consente una varianza come questa:

class FooParent { 
    public function foo(int|false $a): int|bool {} 
} 
class FooChild extends FooParent{ 
    public function foo(int|bool $b): int|false {} 
}

Tag: