PHP 8.0: Attributi (3/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 terzo, esamineremo gli attributi.

Gli attributi introducono un modo completamente nuovo per scrivere metadati strutturati per le classi e tutti i loro membri, nonché per funzioni, closure e i loro parametri. A questo scopo, finora si sono utilizzati i commenti phpDoc, ma la loro sintassi è sempre stata così libera e disomogenea che non era possibile iniziare a elaborarli automaticamente. Pertanto, vengono sostituiti dagli attributi con una sintassi fissa e supporto nelle classi di reflection.

Quindi, le librerie che finora ottenevano metadati analizzando i commenti phpDoc potranno sostituirli con gli attributi. Un esempio è Nette, dove nelle ultime versioni di Application e DI puoi già utilizzare gli attributi Persistent, CrossOrigin e Inject invece 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 usando 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 allo stesso modo come se fossero classi, cioè nel contesto del namespace e delle clausole use. Quindi, sarebbe possibile scriverli ad esempio anche così:

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

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

La scrittura di un singolo attributo assomiglia alla creazione di un'istanza di oggetto, se omettessimo l'operatore new. Cioè, il nome della classe seguito eventualmente da argomenti tra parentesi:

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

E qui si può applicare ottimamente un'altra novità calda di PHP 8.0 – gli argomenti nominati:

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

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

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

#[Inject, Lazy]
public Bar $bar;

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

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

Nei valori predefiniti delle proprietà è possibile utilizzare espressioni semplici e costanti, che possono essere valutate durante la compilazione, e lo stesso vale per gli argomenti degli attributi:

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

Sfortunatamente, il valore di un argomento non può essere un altro attributo, cioè non è possibile annidare gli attributi. Ad esempio, la seguente annotazione utilizzata in Doctrine non può essere trasformata direttamente in attributi:

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

Inoltre, non esiste un equivalente attributivo per il phpDoc a livello di file, cioè il commento che si trova all'inizio del file, che utilizza ad esempio Nette Tester.

Recupero degli attributi

Quali attributi hanno i singoli elementi lo scopriamo tramite la reflection. Le classi di reflection dispongono di 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();      // nome completo dell'attributo, es. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // restituisce l'istanza new MyAttributes\Example('Hello', 123)
}

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

Classi degli attributi

La classe dell'attributo MyAttributes\Example non deve necessariamente esistere. L'esistenza è richiesta solo dalla chiamata del metodo newInstance() perché crea la sua istanza. Scriviamola quindi. Sarà una classe del tutto normale, solo che dobbiamo specificare l'attributo Attribute (cioè dal namespace globale di sistema):

namespace MyAttributes;

use Attribute;

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

È possibile limitare a quali elementi del linguaggio sarà legale utilizzare l'attributo. Ad esempio, così solo per classi e 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 predefinito TARGET_ALL.

Ma attenzione, la verifica dell'uso corretto o errato avviene sorprendentemente solo alla chiamata del metodo newInstance(). Il compilatore stesso non esegue questo controllo.

Futuro con gli attributi

Grazie agli attributi e ai nuovi tipi, i commenti di documentazione PHP diventeranno per la prima volta nella loro storia veramente solo commenti di documentazione. Già ora PhpStorm sta introducendo propri attributi, che possono sostituire ad esempio l'annotazione @deprecated. E si può presumere che questo attributo un giorno sarà standard in PHP. Allo stesso modo verranno sostituite anche altre annotazioni, come @throws ecc.

Sebbene Nette utilizzi le annotazioni fin dalla sua primissima versione per contrassegnare parametri e componenti persistenti, il loro utilizzo più massiccio non è avvenuto perché non si trattava di un costrutto nativo del linguaggio, quindi gli editor non li suggerivano ed era facile commettere errori. Sebbene questo oggi sia risolto dai plugin per gli editor, il percorso veramente nativo offerto dagli attributi apre possibilità completamente nuove.

A proposito, gli attributi hanno ottenuto un'eccezione nel Nette Coding Standard, che richiede che il nome della classe, oltre alla specificità (ad es. Product, InvalidValue), contenga anche la generalità (quindi ProductPresenter, InvalidValueException). Altrimenti, non sarebbe chiaro dall'uso nel codice cosa rappresenta esattamente la classe. Per gli attributi, questo al contrario non è desiderabile, quindi la classe si chiama Inject invece di InjectAttribute.

Nell'ultima parte vedremo quali nuove funzioni e classi sono apparse in PHP e presenteremo il Just in Time Compiler.