PHP 8.0 introdujo los atributos (anteriormente llamados anotaciones en otros lenguajes): metadatos estructurados que se añaden a clases, métodos, propiedades, parámetros y funciones con la sintaxis #[NombreAtributo]. A diferencia de los comentarios DocBlock, los atributos son código PHP real, comprobable en tiempo de compilación y accesible en runtime mediante la Reflection API.
Sintaxis básica
<?php
// Atributo predefinido de PHP
#[Deprecated('Usa nuevaFuncion() en su lugar')]
function funcionVieja(): void { /* ... */ }
// Atributo personalizado en una clase
#[Ruta('/api/usuarios', metodos: ['GET', 'POST'])]
class UsuarioController {
#[Ruta('/api/usuarios/{id}', metodos: ['GET'])]
public function mostrar(int $id): void { /* ... */ }
}
Definir tus propios atributos
Para crear un atributo personalizado, define una clase normal y márcala con #[Attribute]. Puedes restringir en qué tipo de elementos puede usarse con las constantes de Attribute:
<?php
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Ruta {
public function __construct(
public readonly string $path,
public readonly array $metodos = ['GET'],
public readonly string $nombre = ''
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Validar {
public function __construct(
public readonly string $tipo,
public readonly bool $requerido = true,
public readonly int $maxLen = 255
) {}
}
Las constantes disponibles para TARGET_* son: TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER, TARGET_ALL (por defecto).
Atributos repetibles: IS_REPEATABLE
<?php
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Middleware {
public function __construct(public readonly string $clase) {}
}
#[Middleware(AuthMiddleware::class)]
#[Middleware(RateLimitMiddleware::class)]
#[Middleware(CorsMiddleware::class)]
class ApiController {
// ...
}
Leer atributos con la Reflection API
getAttributes() devuelve un array de instancias de ReflectionAttribute. Llama a newInstance() para instanciar el atributo real con sus argumentos:
<?php
$ref = new ReflectionClass(UsuarioController::class);
// Atributos en la clase
foreach ($ref->getAttributes(Ruta::class) as $atributo) {
$ruta = $atributo->newInstance();
echo "Path: {$ruta->path}n";
echo "Métodos: " . implode(', ', $ruta->metodos) . "n";
}
// Atributos en un método específico
$metodo = $ref->getMethod('mostrar');
foreach ($metodo->getAttributes(Ruta::class) as $atributo) {
$ruta = $atributo->newInstance();
echo "Ruta del método: {$ruta->path}n";
}
Ejemplo real: router basado en atributos
<?php
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Get {
public function __construct(public readonly string $path) {}
}
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Post {
public function __construct(public readonly string $path) {}
}
class ProductoController {
#[Get('/productos')]
public function listar(): void { /* ... */ }
#[Get('/productos/{id}')]
public function ver(int $id): void { /* ... */ }
#[Post('/productos')]
public function crear(): void { /* ... */ }
}
// Construir el router leyendo los atributos
function registrarRutas(string $controlador, object $router): void {
$ref = new ReflectionClass($controlador);
foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $metodo) {
foreach ([Get::class, Post::class] as $atributoClase) {
foreach ($metodo->getAttributes($atributoClase) as $attr) {
$inst = $attr->newInstance();
$verbo = class_basename($atributoClase); // 'Get' o 'Post'
$router->add(strtolower($verbo), $inst->path, [$controlador, $metodo->getName()]);
}
}
}
}
Ejemplo real: validador basado en atributos
<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class NotBlank {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Length {
public function __construct(public int $min = 0, public int $max = PHP_INT_MAX) {}
}
class RegistroDto {
#[NotBlank]
#[Length(min: 3, max: 50)]
public string $nombre = '';
#[NotBlank]
public string $email = '';
}
function validar(object $dto): array {
$errores = [];
$ref = new ReflectionClass($dto);
foreach ($ref->getProperties() as $prop) {
$valor = $prop->getValue($dto);
foreach ($prop->getAttributes(NotBlank::class) as $_) {
if (empty($valor)) {
$errores[] = "{$prop->getName()} no puede estar vacío";
}
}
foreach ($prop->getAttributes(Length::class) as $attr) {
$l = $attr->newInstance();
$len = mb_strlen((string) $valor);
if ($len < $l->min || $len > $l->max) {
$errores[] = "{$prop->getName()} debe tener entre {$l->min} y {$l->max} caracteres";
}
}
}
return $errores;
}
$dto = new RegistroDto();
$dto->nombre = 'A';
print_r(validar($dto));
// Array ( [0] => email no puede estar vacío [1] => nombre debe tener entre 3 y 50 caracteres )
