PHP 8.0: novità nei tipi di dati (2/4)

3 anni fa Da David Grudl  

È stata appena rilasciata la versione 8.0 di PHP. È ricca di nuove funzionalità, come nessuna versione precedente. La loro introduzione merita quattro articoli separati. Nella seconda parte, daremo uno sguardo ai tipi di dati.

Facciamo un passo indietro nella storia. L'introduzione dei suggerimenti per i tipi scalari ha rappresentato una svolta significativa in PHP 7. Per poco non è stata realizzata. Andreu Faulds, l'autore dell'ingegnosa soluzione, interamente retrocompatibile e opzionale grazie a declare(strict_types=1), fu duramente respinto dalla comunità. Fortunatamente, Anthony Ferrara difese lei e la proposta all'epoca, lanciò una campagna e l'RFC passò a stretto giro. Whew. La maggior parte delle novità di PHP 8 sono per gentile concessione del leggendario Nikita Popov e tutte hanno superato la votazione come un coltello nel burro. Il mondo sta cambiando in meglio.

PHP 8 porta i tipi alla perfezione. La maggior parte delle annotazioni di phpDoc, come @param, @return e @var, saranno sostituite da notazioni native. Ma soprattutto, i tipi saranno controllati dal motore di PHP. Solo le descrizioni di strutture come string[] o le annotazioni più complesse per PHPStan rimarranno nei commenti.

Tipi di unione

I tipi union sono un'enumerazione di due o più tipi che una variabile può accettare:

class Button
{
	private string|object $caption;

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

Alcuni tipi di unione sono già stati introdotti in PHP. I tipi nulli, per esempio. Come ?string, che è equivalente al tipo union string|null. La notazione a punto interrogativo può essere considerata un'abbreviazione. Naturalmente, funziona anche in PHP 8, ma non si può combinare con le barre verticali. Quindi, invece di ?string|object si deve scrivere string|object|null. Inoltre, iterable è sempre stato equivalente a array|Traversable. Si potrebbe essere sorpresi dal fatto che anche float è un tipo union, in quanto accetta sia int|float, sia float.

Non è possibile utilizzare gli pseudotipi void e mixed nelle unioni, perché non avrebbe senso.

Nette è pronto per i tipi unione. In Schema, Expect::from() li accetta e anche i presentatori li accettano. Si possono usare, ad esempio, nei metodi di rendering e di azione:

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

D'altra parte, il cablaggio automatico in Nette DI rifiuta i tipi di unione. Finora non esiste un caso d'uso in cui abbia senso che il costruttore accetti uno o l'altro oggetto. Naturalmente, se tale caso d'uso dovesse presentarsi, sarà possibile adattare il comportamento del contenitore di conseguenza.

I metodi getParameterType(), getReturnType() e getPropertyType() in Nette\Utils\Reflection lanciano un'eccezione nel caso del tipo union (questo nella versione 3.1; nella precedente versione 3.0, questi metodi ritornano per mantenere la compatibilità con null).

mixed

Lo pseudotipo mixed accetta qualsiasi valore.

Nel caso di parametri e proprietà, si ottiene lo stesso comportamento che si avrebbe se non si specificasse alcun tipo. Qual è il suo scopo? Distinguere quando un tipo è semplicemente mancante e quando è intenzionalmente mixed.

Nel caso dei valori di ritorno di funzioni e metodi, non specificare il tipo è diverso dall'utilizzare mixed. Significa l'opposto di void, poiché richiede che la funzione restituisca qualcosa. Un ritorno mancante produce quindi un errore fatale.

In pratica, si dovrebbe usare raramente, perché grazie ai tipi union è possibile specificare con precisione il valore. È quindi adatto solo in situazioni particolari:

function dump(mixed $var): mixed
{
	// print variable
	return $var;
}

false

A differenza di mixed, il nuovo pseudotipo false può essere utilizzato esclusivamente nei tipi union. È nato dall'esigenza di descrivere il tipo di ritorno delle funzioni native, che storicamente restituiscono false in caso di fallimento:

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

Pertanto, non esiste il tipo true. Non è possibile utilizzare solo false, né false|null, né bool|false.

static

Lo pseudotipo static può essere usato solo come tipo di ritorno di un metodo. Dice che il metodo restituisce un oggetto dello stesso tipo dell'oggetto stesso (mentre self dice che restituisce la classe in cui il metodo è definito). È eccellente per descrivere interfacce fluenti:

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 in futuro. Le risorse sono una reliquia del periodo in cui PHP non aveva oggetti. Gradualmente, le risorse saranno sostituite dagli oggetti. Alla fine, scompariranno completamente. Ad esempio, PHP 8.0 sostituisce la risorsa immagine con l'oggetto GdImage e la risorsa curl con l'oggetto CurlHandle.

Stringable

È un'interfaccia che viene implementata automaticamente da ogni oggetto con un metodo magico __toString().

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

function print(Stringable|string $s)
{
}

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

È possibile dichiarare esplicitamente class Email implements Stringable nella definizione della classe, ma non è necessario.

Nette\Utils\Html riflette anche questo schema di denominazione, implementando l'interfaccia Nette\HtmlStringable invece della precedente IHtmlString. Gli oggetti di questo tipo, ad esempio, non vengono evasi da Latte.

Tipo varianza, controvarianza, covarianza

Il principio di sostituzione di Liskov (LSP) stabilisce che le classi di estensione e le implementazioni di interfacce non devono mai richiedere di più e fornire di meno rispetto al genitore. In altre parole, il metodo figlio non deve richiedere più argomenti o accettare una gamma più ristretta di tipi di parametri rispetto al genitore e, viceversa, non deve restituire una gamma più ampia di tipi. Ma può restituirne di meno. Perché? Perché altrimenti l'ereditarietà si interromperebbe. Una funzione accetterebbe un oggetto di un tipo specifico, ma non avrebbe idea di quali parametri può passare ai suoi metodi e quali tipi restituirebbe. Qualsiasi figlio potrebbe romperla.

Quindi, nell'OOP, la classe figlio può:

  • accettare una gamma più ampia di tipi nei parametri (questa è chiamata contravarianza)
  • restituire una gamma più ristretta di tipi (covarianza)
  • e le proprietà non possono cambiare tipo (sono invarianti).

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

Nel caso di mixed, il figlio può restringere il valore di ritorno a qualsiasi tipo, ma non void, poiché non rappresenta un valore, ma piuttosto la sua assenza. Inoltre, il bambino non può omettere la dichiarazione del tipo, poiché anche in questo caso si può parlare di assenza di valore.

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

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

I tipi di unione possono anche essere estesi 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 esteso a bool nel parametro o viceversa bool a false nel valore di ritorno.

Tutte le infrazioni alla covarianza/contravarianza comportano un errore fatale in PHP 8.0.

Nelle prossime parti di questa serie, mostreremo cosa sono gli attributi, quali nuove funzioni e classi sono apparse in PHP e introdurremo il compilatore Just in Time.