PHP 8.0: Atributos (3/4)

há 4 anos De David Grudl  

A versão 8.0 do PHP está sendo lançada agora mesmo. Ela está cheia de coisas novas como nenhuma outra versão antes. Sua introdução merecia quatro artigos separados. Na terceira parte, vamos dar uma olhada nos atributos.

Os atributos fornecem uma maneira totalmente nova de escrever metadados estruturados para as classes e todos os seus membros, bem como funções, fechamentos e seus parâmetros. Os comentários do PhpDoc têm sido utilizados para este fim até agora, mas sua sintaxe sempre foi tão solta e inconsistente que não foi possível começar a processá-los por máquina. Portanto, eles estão sendo substituídos por atributos com determinada sintaxe e suporte em classes de reflexão.

Por causa disso, as bibliotecas que já recuperaram metadados através da análise dos comentários do phpDoc serão capazes de substituí-los por atributos. Um exemplo é Nette, onde nas últimas versões de Aplicação e DI você já pode usar os atributos Persistent, CrossOrigin e Inject em vez de anotações @persistent, @crossOrigin e @inject.

Código usando anotações:

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

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

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

O mesmo com os atributos:

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()
	{
	}
}

O PHP avalia os nomes de atributos como se fossem classes, no contexto de namespaces e cláusulas use. Assim, seria até possível escrevê-los, por exemplo, como se segue:

use Nette\Application\Attributes;

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

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

A classe que representa o atributo pode ou não existir. Mas é definitivamente melhor se ela existir, porque então o editor pode sugeri-la durante a escrita, o analisador estático reconhece erros de digitação, etc.

Sintaxe

É inteligente que o PHP antes da versão 8 veja os atributos apenas como comentários, de modo que eles também podem ser usados em códigos que devem funcionar em versões mais antigas.

A sintaxe de um atributo individual parece como criar uma instância de objeto se omitirmos o operador new. Portanto, o nome da classe seguido de argumentos opcionais entre parênteses:

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

E aqui é o lugar onde o novo recurso quente do PHP 8.0 pode ser colocado em uso – argumentos nomeados:

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

Cada elemento pode ter múltiplos atributos que podem ser escritos individualmente ou separados por uma vírgula:

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

#[Inject, Lazy]
public Bar $bar;

O seguinte atributo se aplica a todas as três propriedades:

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

Expressões simples e constantes, que podem ser avaliadas durante a compilação e são usadas como valores padrão para propriedades, podem ser usadas como argumentos em atributos:

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

Infelizmente, o valor de um argumento não pode ser outro atributo, ou seja, os atributos não podem ser aninhados. Por exemplo, não há uma maneira simples de converter em atributos a seguinte anotação utilizada em Doutrina:

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

Além disso, não há atributo equivalente para o arquivo phpDoc, ou seja, um comentário localizado no início de um arquivo, que é usado, por exemplo, pelo Nette Tester.

Atributo de reflexão

Quais atributos os elementos individuais podem ser determinados por meio da reflexão. As classes de reflexão têm um novo método getAttributes(), que retorna um conjunto de objetos 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)
}

Os atributos retornados podem ser filtrados por um parâmetro, por exemplo $reflection->getAttributes(Example::class) retorna apenas atributos Example.

Aulas de Atributos

A classe de atributo MyAttributes\Example pode não existir. Somente um método chamado newInstance() requer sua existência porque o instanta. Portanto, vamos escrevê-lo. Será uma classe completamente comum, somente temos que fornecer o atributo Attribute (ou seja, a partir do espaço de nomes do sistema global):

namespace MyAttributes;

use Attribute;

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

Pode ser restringido para quais elementos lingüísticos o uso do atributo será permitido. Por exemplo, apenas para classes e propriedades:

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

As bandeiras TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER e o padrão TARGET_ALL estão disponíveis.

Mas cuidado, a verificação do uso correto ou incorreto ocorre surpreendentemente apenas quando o método newInstance() é chamado. O próprio compilador não realiza esta verificação.

O futuro com atributos

Graças aos atributos e novos tipos, os comentários de documentação PHP pela primeira vez em sua história se tornarão realmente apenas comentários documentais. O PhpStorm já vem com atributos personalizados, que podem substituir, por exemplo, a anotação @deprecated. E pode ser assumido que este atributo estará um dia em PHP por padrão. Da mesma forma, outras anotações serão substituídas, tais como @throws etc.

Embora Nette tenha usado anotações desde sua primeira versão para indicar parâmetros e componentes persistentes, elas não foram usadas de forma mais maciça porque não eram uma construção em língua nativa, então os editores não as sugeriram e foi fácil cometer um erro. Mesmo que isto já esteja sendo tratado pelos plugins do editor, a maneira realmente nativa, que está sendo trazida pelos atributos, abre possibilidades completamente novas.

A propósito, os atributos ganharam uma exceção na Norma de Codificação Nette, que exige que o nome da classe, além da especificidade (por exemplo, Product, InvalidValue), contenha também uma generalidade (ou seja, ProductPresenter, InvalidValueException). Caso contrário, quando usado em código, não ficaria claro o que exatamente a classe representa. Para os atributos, isto não é desejável, portanto a classe é chamada Inject ao invés de InjectAttribute.

Na última parte, veremos quais novas funções e classes apareceram no PHP e apresentaremos o Compilador Just in Time.