Attributes (Atributos)
Los Attributes permiten agregar metadatos estructurados a clases, metodos, propiedades y parametros. Se leen mediante Reflection y son utiles en frameworks y bibliotecas.
Sintaxis basica
Los attributes se declaran con #[NombreAtributo]
justo antes del elemento que decoran:
<?php
declare(strict_types=1);
// Definir un attribute
#[\Attribute]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
// Usar el attribute
#[Route('/usuarios', 'GET')]
class UsuarioController
{
#[Route('/usuarios/{id}', 'GET')]
public function mostrar(int $id): void
{
// ...
}
#[Route('/usuarios', 'POST')]
public function crear(): void
{
// ...
}
}
Leer attributes con Reflection
Los attributes no hacen nada por si solos. Se leen usando la API de Reflection:
<?php
declare(strict_types=1);
#[\Attribute]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
#[Route('/api/productos')]
class ProductoController {}
// Leer el attribute de la clase
$reflector = new \ReflectionClass(ProductoController::class);
$attributes = $reflector->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance(); // Crea instancia del attribute
echo "Path: {$route->path}\n"; // /api/productos
echo "Method: {$route->method}\n"; // GET
}
Attributes en propiedades
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Column
{
public function __construct(
public string $name,
public string $type = 'string'
) {}
}
class Usuario
{
#[Column('user_id', 'int')]
public int $id;
#[Column('user_name')]
public string $nombre;
#[Column('user_email')]
public string $email;
}
// Leer attributes de propiedades
$reflector = new \ReflectionClass(Usuario::class);
foreach ($reflector->getProperties() as $propiedad) {
$attrs = $propiedad->getAttributes(Column::class);
if (!empty($attrs)) {
$column = $attrs[0]->newInstance();
echo "{$propiedad->getName()} -> {$column->name} ({$column->type})\n";
}
}
// id -> user_id (int)
// nombre -> user_name (string)
// email -> user_email (string)
Targets: donde aplicar attributes
Puedes restringir donde se puede usar un attribute:
<?php
declare(strict_types=1);
// Solo en clases
#[\Attribute(\Attribute::TARGET_CLASS)]
class Entity {}
// Solo en metodos
#[\Attribute(\Attribute::TARGET_METHOD)]
class Cache {}
// Solo en propiedades
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Validate {}
// En multiples lugares
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
class Deprecated {}
// Permitir repetir el attribute
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class Tag
{
public function __construct(public string $nombre) {}
}
#[Tag('orm')]
#[Tag('entity')]
class Producto {}
Ejemplo practico: Validacion
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Required {}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Email {}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MinLength
{
public function __construct(public int $min) {}
}
class RegistroDTO
{
#[Required]
#[MinLength(3)]
public string $nombre;
#[Required]
#[Email]
public string $email;
#[Required]
#[MinLength(8)]
public string $password;
}
// Validador simple
function validar(object $objeto): array
{
$errores = [];
$reflector = new \ReflectionClass($objeto);
foreach ($reflector->getProperties() as $prop) {
$prop->setAccessible(true);
$valor = $prop->getValue($objeto);
$nombre = $prop->getName();
// Required
if ($prop->getAttributes(Required::class)) {
if (empty($valor)) {
$errores[] = "$nombre es requerido";
}
}
// Email
if ($prop->getAttributes(Email::class)) {
if (!filter_var($valor, FILTER_VALIDATE_EMAIL)) {
$errores[] = "$nombre debe ser un email valido";
}
}
// MinLength
$minAttrs = $prop->getAttributes(MinLength::class);
if (!empty($minAttrs)) {
$min = $minAttrs[0]->newInstance()->min;
if (strlen($valor) < $min) {
$errores[] = "$nombre debe tener al menos $min caracteres";
}
}
}
return $errores;
}
$dto = new RegistroDTO();
$dto->nombre = 'Jo';
$dto->email = 'no-es-email';
$dto->password = '123';
print_r(validar($dto));
// ['nombre debe tener al menos 3 caracteres',
// 'email debe ser un email valido',
// 'password debe tener al menos 8 caracteres']
Ejercicios
Ejercicio 1: Attribute Deprecated
Crea un attribute Deprecated que reciba un mensaje
y usalo para marcar un metodo como obsoleto.
Ver solucion
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_METHOD)]
class Deprecated
{
public function __construct(
public string $mensaje = 'Este metodo esta obsoleto'
) {}
}
class Calculadora
{
#[Deprecated('Usa sumar() en su lugar')]
public function add(int $a, int $b): int
{
return $a + $b;
}
public function sumar(int $a, int $b): int
{
return $a + $b;
}
}
// Verificar si un metodo esta deprecated
$ref = new \ReflectionMethod(Calculadora::class, 'add');
$attrs = $ref->getAttributes(Deprecated::class);
if (!empty($attrs)) {
$dep = $attrs[0]->newInstance();
echo "Aviso: {$dep->mensaje}";
}
Ejercicio 2: Attribute JsonProperty
Crea un attribute JsonProperty que mapee propiedades
a nombres JSON diferentes.
Ver solucion
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class JsonProperty
{
public function __construct(public string $name) {}
}
class Usuario
{
#[JsonProperty('first_name')]
public string $nombre;
#[JsonProperty('last_name')]
public string $apellido;
public string $email; // Sin attribute, usa nombre original
}
function toJson(object $obj): string
{
$data = [];
$ref = new \ReflectionClass($obj);
foreach ($ref->getProperties() as $prop) {
$prop->setAccessible(true);
$attrs = $prop->getAttributes(JsonProperty::class);
$key = !empty($attrs)
? $attrs[0]->newInstance()->name
: $prop->getName();
$data[$key] = $prop->getValue($obj);
}
return json_encode($data);
}
$u = new Usuario();
$u->nombre = 'Ana';
$u->apellido = 'Garcia';
$u->email = 'ana@example.com';
echo toJson($u);
// {"first_name":"Ana","last_name":"Garcia","email":"ana@example.com"}
¿Has encontrado un error o tienes una sugerencia?
Escribenos