Atributos en PHP 8: #[Attribute], crear tus propios atributos y leerlos con Reflection

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 )

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP