PHP 8.0: Atributos (3/4)

há 4 anos De David Grudl  

A versão 8.0 do PHP foi lançada. Está tão cheia de novidades como nenhuma versão anterior. A apresentação delas exigiu quatro artigos separados. Neste terceiro, veremos os atributos.

Os atributos trazem uma maneira completamente nova de escrever metadados estruturados para classes e todos os seus membros, bem como para funções, closures e seus parâmetros. Para este fim, até agora eram usados comentários phpDoc, mas sua sintaxe sempre foi tão livre e inconsistente que não era possível começar a processá-los automaticamente. Por isso, são substituídos por atributos com sintaxe fixa e suporte em classes de reflexão.

Portanto, bibliotecas que até agora obtinham metadados analisando comentários phpDoc poderão substituí-los por atributos. Um exemplo é o Nette, onde nas versões mais recentes do Application e DI, você já pode usar os atributos Persistent, CrossOrigin e Inject em vez das 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 usando 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 dos atributos da mesma forma que se fossem classes, ou seja, no contexto do namespace e das cláusulas use. Assim, seria possível escrevê-los, por exemplo, desta forma:

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 existir ou não. Mas é certamente melhor que exista, porque então o editor pode sugeri-la ao escrever, o analisador estático reconhece erros de digitação, etc.

Sintaxe

O que é útil é que o PHP antes da versão 8 vê os atributos apenas como comentários, então eles podem ser usados ​​mesmo em código que deve funcionar em versões mais antigas.

A notação de um único atributo parece a criação de uma instância de objeto, se omitirmos o operador new. Ou seja, o nome da classe seguido por argumentos 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 vários atributos, que podem ser escritos separadamente ou separados por 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;

Nos valores padrão das propriedades, podem ser usadas expressões simples e constantes que podem ser avaliadas durante a compilação, e o mesmo se aplica aos argumentos dos 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, não é possível aninhar atributos. Por exemplo, a seguinte anotação usada no Doctrine não pode ser transformada diretamente em atributos:

/**
 * @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.

Obtendo atributos

Descobrimos quais atributos os elementos individuais possuem usando reflexão. As classes de reflexão possuem um novo método getAttributes(), que retorna um array de objetos ReflectionAttribute.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // nome completo do atributo, ex: MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // retorna a instância new MyAttributes\Example('Hello', 123)
}

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

Classes de atributos

A classe de atributo MyAttributes\Example não precisa existir. A existência é exigida apenas pela chamada do método newInstance(), porque cria sua instância. Vamos então escrevê-la. Será uma classe completamente normal, apenas precisamos indicar o atributo Attribute nela (ou seja, do namespace global do sistema):

namespace MyAttributes;

use Attribute;

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

É possível restringir em quais elementos da linguagem será legal usar o atributo. Assim, por exemplo, apenas em classes e propriedades:

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

Estão disponíveis as flags TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER e o padrão TARGET_ALL.

Mas atenção, a verificação do uso correto ou incorreto ocorre surpreendentemente apenas na chamada do método newInstance(). O próprio compilador não realiza essa verificação.

O futuro com atributos

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

Embora o Nette use anotações desde sua primeira versão para marcar parâmetros e componentes persistentes, seu uso mais massivo não ocorreu porque não era uma construção nativa da linguagem, então os editores não as sugeriam e era fácil cometer erros nelas. Embora isso seja resolvido hoje por plugins de editor, o caminho verdadeiramente nativo que os atributos trazem abre possibilidades completamente novas.

A propósito, os atributos receberam uma exceção no Nette Coding Standard, que exige que o nome da classe, além da especificidade (por exemplo, Product, InvalidValue), também contenha generalidade (ou seja, ProductPresenter, InvalidValueException). Caso contrário, não ficaria claro no uso no código o que exatamente a classe representa. Para atributos, isso, por outro lado, não é desejável, então a classe se chama Inject em vez de InjectAttribute.

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