PHP 8.0: Attributi (3/4)

3 anni fa Da David Grudl  

La versione 8.0 di PHP viene rilasciata proprio in questi giorni. È piena di novità come nessun'altra versione prima. La sua introduzione ha meritato quattro articoli separati. Nella terza parte daremo un'occhiata agli attributi.

Gli attributi forniscono un modo completamente nuovo di scrivere metadati strutturati per le classi e tutti i loro membri, nonché per le funzioni, le chiusure e i loro parametri. I commenti di PhpDoc sono stati usati a questo scopo finora, ma la loro sintassi è sempre stata così libera e incoerente che non era possibile iniziare a elaborarli. Pertanto, sono stati sostituiti da attributi con una sintassi determinata e con il supporto delle classi di riflessione.

Per questo motivo, le librerie che in precedenza recuperavano i metadati analizzando i commenti di phpDoc saranno in grado di sostituirli con gli attributi. Un esempio è Nette, dove nelle ultime versioni di Application e DI è già possibile utilizzare gli attributi Persistent, CrossOrigin e Inject al posto delle annotazioni @persistent, @crossOrigin e @inject.

Codice che utilizza le annotazioni:

/**
 * @persistent(comp1, comp2)
 */
class SomePresenter
{
	/** @persistent */
	public $page = 1;

	/** @inject */
	public Facade $facade;

	/**
	 * @crossOrigin
	 */
	public function handleSomething()
	{
	}
}

Lo stesso vale per gli attributi:

use Nette\Application\Attributes\CrossOrigin;
use Nette\Application\Attributes\Persistent;
use Nette\DI\Attributes\Inject;

#[Persistent('comp1', 'comp2')]
class SomePresenter
{
	#[Persistent]
	public int $page = 1;

	#[Inject]
	public Facade $facade;

	#[CrossOrigin]
	public function handleSomething()
	{
	}
}

PHP valuta i nomi degli attributi come se fossero classi, nel contesto degli spazi dei nomi e delle clausole use. Quindi sarebbe possibile scriverli, per esempio, come segue:

use Nette\Application\Attributes;

class SomePresenter
{
	#[Attributes\Persistent]
	public int $page = 1;

	#[\Nette\DI\Attributes\Inject]
	public Facade $facade;

La classe che rappresenta l'attributo può esistere o meno. Ma è sicuramente meglio se esiste, perché così l'editor può suggerirla durante la scrittura, l'analizzatore statico riconosce gli errori di battitura, ecc.

Sintassi

È intelligente che PHP prima della versione 8 veda gli attributi solo come commenti, quindi possono essere usati anche nel codice che dovrebbe funzionare nelle versioni precedenti.

La sintassi di un singolo attributo assomiglia alla creazione di un'istanza di un oggetto, se si omette l'operatore new. Quindi, il nome della classe seguito da argomenti opzionali tra parentesi:

#[Column('string', 32, true, false)]#
protected $username;

Ed è qui che si può utilizzare la nuova caratteristica di PHP 8.0: gli argomenti con nome:

#[Column(
	type: 'string',
	length: 32,
	unique: true,
	nullable: false,
)]#
protected $username;

Ogni elemento può avere più attributi che possono essere scritti singolarmente o separati da una virgola:

#[Inject]
#[Lazy]
public Foo $foo;

#[Inject, Lazy]
public Bar $bar;

Il seguente attributo si applica a tutte e tre le proprietà:

#[Common]
private $a, $b, $c;

Le espressioni e le costanti semplici, che possono essere valutate durante la compilazione e che sono usate come valori predefiniti per le proprietà, possono essere usate come argomenti negli attributi:

#[
	ScalarExpression(1 + 1),
	ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
	BitShift(4 >> 1, 4 << 1),
	BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]

Purtroppo, il valore di un argomento non può essere un altro attributo, cioè gli attributi non possono essere annidati. Per esempio, non esiste un modo semplice per convertire in attributi la seguente annotazione usata in Doctrine:

/**
 * @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
 */

Inoltre, non esiste un attributo equivalente per il file phpDoc, cioè un commento posto all'inizio di un file, usato, per esempio, da Nette Tester.

Riflessione sull'attributo

È possibile determinare quali attributi hanno i singoli elementi utilizzando la riflessione. Le classi Reflection hanno un nuovo metodo getAttributes() che restituisce un array di oggetti ReflectionAttribute.

use MyAttributes\Example;

#[Example('Hello', 123)]
class Foo
{}

$reflection = new ReflectionClass(Foo::class);

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // full attribute name, e.g. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // returns an instance: new MyAttributes\Example('Hello', 123)
}

Gli attributi restituiti possono essere filtrati da un parametro, ad esempio $reflection->getAttributes(Example::class) restituisce solo gli attributi Example.

Classi di attributi

La classe di attributi MyAttributes\Example può non esistere. Solo una chiamata di metodo newInstance() ne richiede l'esistenza, perché la istanzia. Quindi scriviamola. Sarà una classe completamente normale, solo che dobbiamo fornire l'attributo Attribute (cioè dallo spazio dei nomi del sistema globale):

namespace MyAttributes;

use Attribute;

#[Attribute]
class Example
{
	public function __construct(string $message, int $number)
	{
		...
	}
}

Si può limitare l'uso dell'attributo a quali elementi linguistici è consentito. Ad esempio, solo per le classi e le proprietà:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
	...
}

Sono disponibili i flag TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER e il default TARGET_ALL.

Ma attenzione, la verifica dell'uso corretto o scorretto avviene sorprendentemente solo quando viene chiamato il metodo newInstance(). Il compilatore stesso non esegue questo controllo.

Il futuro con gli attributi

Grazie agli attributi e ai nuovi tipi, per la prima volta nella loro storia i commenti alla documentazione di PHP diventeranno davvero solo commenti documentali. PhpStorm è già dotato di attributi personalizzati, che possono sostituire, ad esempio, l'annotazione @deprecated. E si può ipotizzare che un giorno questo attributo sarà presente in PHP per impostazione predefinita. Allo stesso modo, saranno sostituite altre annotazioni, come @throws ecc.

Sebbene Nette abbia utilizzato le annotazioni fin dalla sua prima versione per indicare parametri e componenti persistenti, non sono state utilizzate in modo più massiccio perché non erano un costrutto del linguaggio nativo, quindi i redattori non le suggerivano ed era facile commettere un errore. Anche se questo aspetto è già stato affrontato con i plugin per gli editor, il modo veramente nativo, portato dagli attributi, apre possibilità completamente nuove.

Tra l'altro, gli attributi hanno ottenuto un'eccezione nello standard di codifica Nette, che richiede che il nome della classe, oltre alla specificità (ad esempio Product, InvalidValue), contenga anche una generalità (ad esempio ProductPresenter, InvalidValueException). Altrimenti, quando viene utilizzato nel codice, non sarebbe chiaro cosa rappresenta esattamente la classe. Per gli attributi, questo non è auspicabile, quindi la classe si chiama Inject invece di InjectAttribute.

Nell'ultima parte, vedremo quali nuove funzioni e classi sono apparse in PHP e introdurremo il compilatore Just in Time.