PHP 8.0: Atributos (3/4)

hace 4 años por David Grudl  

Ha salido la versión 8.0 de PHP. Está tan repleta de novedades como ninguna versión anterior. Su presentación ha requerido cuatro artículos separados. En este tercero, veremos los atributos.

Los atributos traen una forma completamente nueva de escribir metadatos estructurados para clases y todos sus miembros, así como para funciones, closures y sus parámetros. Para este propósito, hasta ahora se utilizaban comentarios phpDoc, pero su sintaxis siempre fue tan libre e inconsistente que no era posible empezar a procesarlos automáticamente. Por eso son reemplazados por atributos con sintaxis fija y soporte en clases de reflexión.

Por lo tanto, las librerías que hasta ahora obtenían metadatos parseando comentarios phpDoc podrán reemplazarlos por atributos. Un ejemplo es Nette, donde en las últimas versiones de Application y DI ya puede usar los atributos Persistent, CrossOrigin e Inject en lugar de las anotaciones @persistent, @crossOrigin y @inject.

Código usando anotaciones:

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

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

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

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

PHP evalúa los nombres de los atributos de la misma manera que si fueran clases, es decir, en el contexto del espacio de nombres y las cláusulas use. Por lo tanto, sería posible escribirlos, por ejemplo, también así:

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 definitivamente es mejor si existe, porque entonces el editor puede sugerirla al escribir, el analizador estático puede detectar errores tipográficos, etc.

Sintaxis

Lo práctico es que PHP antes de la versión 8 ve los atributos solo como comentarios, por lo que se pueden usar también en código que debe funcionar en versiones anteriores.

La notación de un atributo individual parece la creación de una instancia de objeto, si omitiéramos el operador new. Es decir, el nombre de la clase seguido opcionalmente por argumentos 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 se pueden escribir por separado o separados por comas:

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

#[Inject, Lazy]
public Bar $bar;

El siguiente atributo se aplica a las tres propiedades:

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

En los valores predeterminados de las propiedades se pueden usar expresiones simples y constantes que se pueden evaluar durante la compilación, y lo mismo se aplica a los argumentos de 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),
]

Desafortunadamente, el valor de un argumento no puede ser otro atributo, es decir, no se pueden anidar atributos. Por ejemplo, la siguiente anotación utilizada en Doctrine no se puede transformar directamente en atributos de forma sencilla:

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

Obtención de atributos

Podemos averiguar qué atributos tienen los elementos individuales usando reflexión. Las clases de reflexión disponen del nuevo método getAttributes(), que devuelve un array de objetos ReflectionAttribute.

use MyAttributes\Example;

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

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // nombre completo del atributo, p.ej. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // devuelve la instancia new MyAttributes\Example('Hello', 123)
}

Los atributos devueltos se pueden filtrar por parámetro, por ejemplo, $reflection->getAttributes(Example::class) devolverá solo los atributos Example.

Clases de atributos

La clase de atributo MyAttributes\Example no tiene por qué existir. Su existencia solo es requerida por la llamada al método newInstance() porque crea su instancia. Así que vamos a escribirla. Será una clase completamente normal, solo que debemos indicar el atributo Attribute (es decir, del espacio de nombres global del sistema):

namespace MyAttributes;

use Attribute;

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

Se puede limitar a qué elementos del lenguaje será legal aplicar el atributo. Así, por ejemplo, solo a clases y propiedades:

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

Están disponibles los flags TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER y el predeterminado TARGET_ALL.

Pero cuidado, la verificación del uso correcto o incorrecto ocurre sorprendentemente solo al llamar al método newInstance(). El propio compilador no realiza esta verificació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 utiliza anotaciones desde su primera versión para marcar parámetros y componentes persistentes, su uso masivo no se produjo porque no era una construcción nativa del lenguaje, por lo que los editores no las sugerían y era fácil cometer errores en ellas. Aunque esto hoy en día lo solucionan los plugins para editores, el camino realmente nativo que traen los atributos abre posibilidades completamente nuevas.

Por cierto, los atributos obtuvieron una excepción en el Nette Coding Standard, que requiere que el nombre de la clase, además de la especificidad (por ejemplo, Product, InvalidValue), contenga también la generalidad (es decir, ProductPresenter, InvalidValueException). De lo contrario, no quedaría claro al usarlo en el código qué representa exactamente la clase. En los atributos, esto, por el contrario, no es deseable, por lo que la clase se llama Inject en lugar de InjectAttribute.

En la última entrega veremos qué nuevas funciones y clases han aparecido en PHP y presentaremos el Just in Time Compiler.