PHP 8.0: Tipi di dati (2/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 secondo, esamineremo i tipi di dati.

Torniamo alla storia. La svolta fondamentale di PHP 7 è stata l'introduzione dei type hint scalari. Per poco non è successo. L'autrice della straordinaria soluzione Andrea Faulds, che grazie a declare(strict_types=1) era completamente retrocompatibile e opzionale, è stata respinta bruscamente dalla comunità. Fortunatamente, Anthony Ferrara ha preso le sue difese e quelle della sua proposta, ha lanciato una campagna e l'RFC è passata per un pelo. Ufff. La maggior parte delle novità in PHP 8 è opera del leggendario Nikita Popov e nelle votazioni gli sono passate lisce come l'olio. Il mondo sta cambiando in meglio.

PHP 8 porta i tipi alla perfezione. Scomparirà la stragrande maggioranza delle annotazioni phpDoc @param, @return e @var nel codice e saranno sostituite dalla notazione nativa e soprattutto dal controllo da parte del motore PHP. Nei commenti rimarranno solo le descrizioni di strutture come string[] o annotazioni più complesse per PHPStan.

Union Types

I tipi unione sono un elenco di due o più tipi che un valore può assumere:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

PHP conosceva già alcuni tipi unione speciali in precedenza. Ad esempio, i tipi nullable come ?string, che è l'equivalente del tipo unione string|null e la notazione con il punto interrogativo può essere considerata solo una scorciatoia. Ovviamente funziona anche in PHP 8, ma non può essere combinato con le barre verticali, quindi invece di ?string|object è necessario scrivere il completo string|object|null. Inoltre, iterable è sempre stato l'equivalente di array|Traversable. Potrebbe sorprendervi che anche float sia in realtà un tipo unione che accetta int|float, ma effettua il cast a float.

Negli unioni non è possibile utilizzare gli pseudo-tipi void e mixed, perché non avrebbe alcun senso.

Nette è pronto per i tipi unione. In Schema, Expect::from() li comprende: Expect::from(), li comprendono anche i presenter, quindi puoi usarli ad esempio nei metodi render e action:

public function renderDetail(int|array $id)
{
	...
}

Al contrario, l'autowiring in Nette DI rifiuta i tipi unione. Manca ancora un caso d'uso in cui avrebbe senso che, ad esempio, un costruttore accettasse o questo o quell'oggetto. Ovviamente, se dovesse emergere, sarà possibile modificare di conseguenza il comportamento del container.

I metodi getParameterType(), getReturnType() e getPropertyType() in Nette\Utils\Reflection lanciano un'eccezione in caso di tipo unione (nella versione 3.1, nella precedente 3.0 restituiscono null per compatibilità).

mixed

Lo pseudo-tipo mixed indica che il valore può essere assolutamente qualsiasi cosa.

Nel caso di parametri e proprietà, si tratta in realtà dello stesso comportamento di quando non specifichiamo alcun tipo. A cosa serve allora? Per poter distinguere quando il tipo semplicemente manca e quando è veramente mixed.

Nel caso del valore di ritorno di una funzione o di un metodo, non specificare il tipo differisce dallo specificare il tipo mixed. Si tratta in realtà dell'opposto di void, poiché indica che la funzione deve restituire qualcosa. Un return mancante è quindi un errore fatale.

In pratica, dovresti usarlo raramente, perché grazie ai tipi unione puoi specificare il valore in modo più preciso. È quindi utile in situazioni eccezionali:

function dump(mixed $var): mixed
{
	// stampa la variabile
	return $var;
}

false

Il nuovo pseudo-tipo false può invece essere utilizzato solo nei tipi unione. È nato dalla necessità di descrivere nativamente il tipo del valore di ritorno delle funzioni native, che storicamente restituiscono false in caso di fallimento:

function strpos(string $haystack, string $needle): int|false
{
}

Per questo motivo, non esiste il tipo true, non è possibile utilizzare nemmeno false da solo o false|null o bool|false.

static

Lo pseudo-tipo static può essere utilizzato solo come tipo di ritorno di un metodo. Indica che il metodo restituisce un oggetto dello stesso tipo dell'oggetto stesso (mentre self indica che restituisce la classe in cui è definito il metodo). Il che è ottimo per descrivere le interfacce fluent:

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Questo tipo non esiste in PHP 8 e non sarà introdotto nemmeno in futuro. Le risorse sono una reliquia storica dei tempi in cui PHP non aveva ancora oggetti. Gradualmente, le risorse verranno sostituite da oggetti e col tempo questo tipo scomparirà completamente. Ad esempio, PHP 8.0 sostituisce la risorsa che rappresenta un'immagine con l'oggetto GdImage e la risorsa di connessione curl con l'oggetto CurlHandle.

Stringable

Si tratta di un'interfaccia che viene implementata automaticamente da ogni oggetto con il metodo magico __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

Nella definizione della classe è possibile specificare esplicitamente class Email implements Stringable, ma non è necessario.

Questo stile di denominazione si riflette anche in Nette\Utils\Html, che implementa l'interfaccia Nette\HtmlStringable invece della precedente IHtmlString. Oggetti di questo tipo, ad esempio, non vengono escapati da Latte.

Varianza dei tipi, controvarianza, covarianza

Il principio di sostituibilità di Liskov (Liskov Substitution Principle – LSP) afferma che i discendenti di una classe e le implementazioni di un'interfaccia non devono mai richiedere di più e fornire di meno del genitore. Cioè, che il metodo di un discendente non deve richiedere più argomenti o accettare per i parametri un intervallo di tipi più ristretto rispetto all'antenato e, al contrario, non deve restituire un intervallo di tipi più ampio. Ma può restituire un intervallo più ristretto. Perché? Perché altrimenti l'ereditarietà non funzionerebbe affatto. Una funzione accetterebbe sì un oggetto di un certo tipo, ma non saprebbe quali parametri passare ai metodi e cosa restituiranno effettivamente, perché qualsiasi discendente potrebbe infrangerlo.

Quindi, in OOP vale che un discendente può:

  • nei parametri accettare un intervallo di tipi più ampio (questo si chiama controvarianza)
  • restituire un intervallo di tipi più ristretto (covarianza)
  • e le proprietà non possono cambiare tipo (sono invarianti)

PHP è in grado di farlo dalla versione 7.4 e tutti i tipi appena introdotti in PHP 8.0 supportano anche la controvarianza e la covarianza.

Nel caso di mixed, un discendente può restringere il valore di ritorno a qualsiasi tipo, ma non void, perché non si tratta di un tipo di valore, ma della sua assenza. Nemmeno un discendente può non specificare il tipo, perché anche questo ammette l'assenza.

class A
{
	public function foo(mixed $foo): mixed
	{}
}

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

Anche i tipi unione possono essere ampliati nei parametri e ristretti nei valori di ritorno:

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

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

Inoltre, false può essere ampliato in un parametro a bool o, al contrario, bool in un valore di ritorno può essere ristretto a false.

Tutte le violazioni della covarianza/controvarianza portano in PHP 8.0 a un fatal error.

Nelle prossime parti mostreremo cosa sono gli attributi, quali nuove funzioni e classi sono apparse in PHP e presenteremo il Just in Time Compiler.