PHP 8.0: Atributos (3/4)

hace 3 años por David Grudl  

La versión 8.0 de PHP está siendo liberada ahora mismo. Está llena de cosas nuevas como ninguna otra versión antes. Su introducción mereció cuatro artículos separados. En la tercera parte echaremos un vistazo a los atributos.

Los atributos proporcionan una forma completamente nueva de escribir metadatos estructurados para clases y todos sus miembros, así como funciones, cierres y sus parámetros. Los comentarios PhpDoc se han utilizado para este propósito hasta ahora, pero su sintaxis siempre ha sido tan floja e inconsistente que no era posible empezar a procesarlos maquinalmente. Por ello, están siendo sustituidos por atributos con sintaxis determinada y soporte en clases de reflexión.

Debido a esto, las bibliotecas que previamente han recuperado metadatos parseando comentarios phpDoc podrán reemplazarlos por atributos. Un ejemplo es Nette, donde en las últimas versiones de Application y DI ya se pueden utilizar los atributos Persistent, CrossOrigin y Inject en lugar de las anotaciones @persistent, @crossOrigin y @inject.

Código que utiliza anotaciones:

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

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

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

Lo mismo con los 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()
	{
	}
}

PHP evalúa los nombres de los atributos igual que si fueran clases, en el contexto de los espacios de nombres y las cláusulas use. Así que incluso sería posible escribirlos, por ejemplo, de la siguiente manera:

use Nette\Application\Attributes;

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

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

La clase que representa el atributo puede existir o no. Pero es definitivamente mejor si existe, porque entonces el editor puede sugerirla durante la escritura, el analizador estático reconoce errores tipográficos, etc.

Sintaxis

Es inteligente que PHP antes de la versión 8 vea los atributos sólo como comentarios, por lo que también pueden ser usados en código que debería funcionar en versiones anteriores.

La sintaxis de un atributo individual se parece a crear una instancia de objeto si omitimos el operador new. Entonces, el nombre de la clase seguido de argumentos opcionales entre paréntesis:

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

Y aquí es donde la nueva característica caliente de PHP 8.0 se puede poner en uso – argumentos con nombre:

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

Cada elemento puede tener múltiples atributos que pueden ser escritos individualmente o separados por una coma:

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

#[Inject, Lazy]
public Bar $bar;

El siguiente atributo se aplica a las tres propiedades:

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

Las expresiones y constantes simples, que pueden evaluarse durante la compilación y se utilizan como valores por defecto para las propiedades, pueden utilizarse como argumentos en los atributos:

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

Lamentablemente, el valor de un argumento no puede ser otro atributo, es decir, los atributos no pueden anidarse. Por ejemplo, no existe una forma directa de convertir en atributos la siguiente anotación utilizada en Doctrine:

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

Tampoco existe un atributo equivalente para el archivo phpDoc, es decir, un comentario situado al principio de un archivo, que es utilizado, por ejemplo, por Nette Tester.

Reflejo del atributo

Qué atributos tienen los elementos individuales se puede determinar mediante el uso de la reflexión. Las clases de reflexión tienen un nuevo método getAttributes(), que devuelve una matriz 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)
}

Los atributos devueltos pueden filtrarse mediante un parámetro, por ejemplo, $reflection->getAttributes(Example::class) devuelve sólo atributos Example.

Clases de atributos

La clase de atributo MyAttributes\Example puede no existir. Sólo una llamada a un método newInstance() requiere su existencia porque la instancia. Así que vamos a escribirla. Será una clase completamente ordinaria, sólo tenemos que proporcionar el atributo Attribute (es decir, del espacio de nombres del sistema global):

namespace MyAttributes;

use Attribute;

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

Se puede restringir para qué elementos del lenguaje se permitirá el uso del atributo. Por ejemplo, sólo para clases y propiedades:

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

Banderas TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER y por defecto TARGET_ALL están disponibles.

Pero cuidado, la verificación del uso correcto o incorrecto se produce sorprendentemente sólo cuando se llama al método newInstance(). El propio compilador no realiza esta comprobación.

El futuro con atributos

Gracias a los atributos y a los nuevos tipos, los comentarios de la documentación PHP por primera vez en su historia se convertirán realmente sólo en comentarios documentales. PhpStorm ya viene con atributos personalizados, que pueden reemplazar, por ejemplo, la anotación @deprecated. Y es de suponer que este atributo estará algún día en PHP por defecto. Del mismo modo, otras anotaciones serán reemplazadas, como @throws etc.

Aunque Nette ha estado usando anotaciones desde su primera versión para indicar parámetros y componentes persistentes, no se han usado más masivamente porque no eran una construcción nativa del lenguaje, por lo que los editores no las sugerían y era fácil cometer un error. Aunque esto ya se está solucionando con plugins del editor, la forma realmente nativa que están aportando los atributos abre posibilidades completamente nuevas.

Por cierto, los atributos han obtenido una excepción en el estándar de codificación Nette, que exige que el nombre de la clase, además de especificidad (por ejemplo, Product, InvalidValue), contenga también una generalidad (por ejemplo, ProductPresenter, InvalidValueException). De lo contrario, al utilizarlo en el código, no quedaría claro qué representa exactamente la clase. En el caso de los atributos, esto no es deseable, por lo que la clase se denomina Inject en lugar de InjectAttribute.

En la última parte, veremos qué nuevas funciones y clases han aparecido en PHP e introduciremos el Compilador Just in Time.