Durante anos, los proyectos PHP grandes dependieron de docblocks para expresar metadatos: rutas, validaciones, mapeos ORM o reglas de inyeccion de dependencias se declaraban en comentarios especiales y se leian con un parser propio. Ese enfoque tenia dos grandes problemas: los comentarios no forman parte del lenguaje y su analisis estatico era limitado, y las herramientas necesitaban reinventar el tratamiento de estas anotaciones de forma incompatible entre si.
PHP 8.0 introdujo los atributos nativos como ciudadanos de primera clase del lenguaje. Los atributos son clases normales que se asocian a declaraciones del codigo con la sintaxis #[Atributo]. El motor los parsea, los expone via reflexion y los verifica igual que cualquier otro simbolo.
Declarar atributos
Un atributo es simplemente una clase marcada con el atributo Attribute. Se pueden declarar propiedades, tipos y un constructor para recibir argumentos.
<?php
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class NoVacio
{
public function __construct(public readonly string $mensaje = "No puede estar vacio") {}
}
El argumento Attribute::TARGET_PROPERTY declara que este atributo solo puede aplicarse a propiedades. Existen otros destinos como TARGET_CLASS, TARGET_METHOD, TARGET_FUNCTION, TARGET_PARAMETER o la combinacion TARGET_ALL.
El argumento Attribute::IS_REPEATABLE indica que un mismo atributo puede aparecer varias veces sobre el mismo destino. Es util para reglas de validacion compuestas.
Aplicar atributos al codigo
Una vez declarado, el atributo se aplica con la sintaxis #[...] justo antes de la declaracion afectada. Puedes pasar argumentos al constructor del atributo entre parentesis.
<?php
final class Usuario
{
public function __construct(
#[NoVacio]
public string $email,
#[NoVacio(mensaje: "Falta el nombre")]
public string $nombre,
) {}
}
Los named arguments estudiados en otras lecciones funcionan tambien al aplicar atributos. Esto hace que la configuracion sea explicita y legible, sin depender de la posicion de los parametros.
Un atributo puede aplicarse a clases, metodos, funciones, parametros, propiedades, constantes de clase e incluso al enum.
<?php
#[Attribute(Attribute::TARGET_CLASS)]
final class Entidad
{
public function __construct(public readonly string $tabla) {}
}
#[Entidad(tabla: "usuarios")]
final class Usuario
{
// ...
}
Leer atributos por reflexion
Los atributos por si solos no hacen nada, son metadatos inertes hasta que alguien los lee. La API Reflection ofrece el metodo getAttributes() en ReflectionClass, ReflectionMethod, ReflectionProperty y demas, que devuelve un array de ReflectionAttribute.
<?php
$reflex = new ReflectionClass(Usuario::class);
foreach ($reflex->getProperties() as $propiedad) {
foreach ($propiedad->getAttributes(NoVacio::class) as $atributo) {
$instancia = $atributo->newInstance();
echo $propiedad->getName() . ": " . $instancia->mensaje . "\n";
}
}
El metodo newInstance() crea un objeto del atributo con los argumentos que se usaron en la aplicacion. Filtrar por clase (NoVacio::class) permite buscar solo los atributos relevantes para una funcionalidad concreta.
El tiempo de arranque se mantiene bajo porque PHP no instancia los atributos a menos que alguien llame a
newInstance(). Esto difiere de otros sistemas de anotaciones que parseaban todo por adelantado.
Validacion declarativa
El caso practico mas comun es la validacion. Declaras reglas mediante atributos sobre las propiedades de un DTO y un validador las lee y ejecuta.
<?php
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class Regla
{
public function __construct(
public readonly string $tipo,
public readonly int|string|null $valor = null,
) {}
}
final class RegistroDTO
{
public function __construct(
#[Regla("email")]
public string $email,
#[Regla("minLength", 8)]
#[Regla("maxLength", 64)]
public string $password,
) {}
}
Un servicio de validacion recorreria las propiedades, leeria los atributos Regla y aplicaria cada comprobacion. El DTO queda autodescriptivo: lo que valida y como se valida viven en el mismo sitio.
<?php
function validar(object $dto): array
{
$errores = [];
$reflex = new ReflectionObject($dto);
foreach ($reflex->getProperties() as $propiedad) {
$valor = $propiedad->getValue($dto);
foreach ($propiedad->getAttributes(Regla::class) as $attr) {
$regla = $attr->newInstance();
$ok = match ($regla->tipo) {
"email" => filter_var($valor, FILTER_VALIDATE_EMAIL) !== false,
"minLength" => strlen($valor) >= $regla->valor,
"maxLength" => strlen($valor) <= $regla->valor,
default => true,
};
if (!$ok) {
$errores[$propiedad->getName()][] = $regla->tipo;
}
}
}
return $errores;
}
Esta abstraccion de una decena de lineas resuelve el caso base. Los frameworks como Symfony Validator o Laravel ofrecen catalogos mucho mas amplios y cuidadosos, pero el mecanismo subyacente es exactamente este.
Atributos para routing
Los routers modernos registran sus endpoints con atributos directamente en los metodos del controlador. El arranque de la aplicacion escanea los controladores y construye la tabla de rutas.
<?php
#[Attribute(Attribute::TARGET_METHOD)]
final class Ruta
{
public function __construct(
public readonly string $metodo,
public readonly string $path,
) {}
}
final class ProductoController
{
#[Ruta("GET", "/productos")]
public function listar(): array { return []; }
#[Ruta("POST", "/productos")]
public function crear(): void {}
#[Ruta("GET", "/productos/{id}")]
public function detalle(int $id): ?object { return null; }
}
Este estilo declarativo mantiene la informacion de routing junto a la implementacion, lo que facilita mover acciones entre controladores o deprecar endpoints.
flowchart LR
A[Clase controlador] -->|Reflexion| B[Attribute Ruta]
B --> C[Tabla de rutas]
C --> D[Resolucion HTTP]
E[Entidad] -->|Reflexion| F[Attribute Entidad]
F --> G[Mapeo ORM]
Atributos del propio PHP y PHPUnit
PHP define varios atributos propios y los frameworks aportan los suyos. Algunos ejemplos habituales incluyen #[Override] para declarar que un metodo sobrescribe uno heredado, con verificacion en tiempo de compilacion, y #[Deprecated] para marcar simbolos que pronto desapareceran.
<?php
class Base
{
public function saludar(): string
{
return "hola";
}
}
class Hija extends Base
{
#[Override]
public function saludar(): string
{
return "hola desde la hija";
}
}
Si alguien renombra el metodo en la clase base, #[Override] convierte el silencio en un error detectable, algo que antes requeria analisis estatico externo.
PHPUnit 10 y versiones posteriores sustituyen las antiguas anotaciones en docblock por atributos reales. Esto elimina la dependencia de un parser propio y alinea la definicion de tests con el resto del codigo.
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;
final class CalculadoraTest extends TestCase
{
#[Test]
public function suma_correcta(): void
{
$this->assertSame(5, 2 + 3);
}
public static function casosDeSuma(): array
{
return [[1, 1, 2], [2, 3, 5], [10, 20, 30]];
}
#[Test]
#[DataProvider("casosDeSuma")]
public function suma_parametrizada(int $a, int $b, int $esperado): void
{
$this->assertSame($esperado, $a + $b);
}
}
Al escribir nuevos tests con PHPUnit, usar los atributos oficiales en lugar de comentarios
@testo@dataProviderasegura compatibilidad con las versiones actuales y con las proximas.
Buenas practicas
Los atributos son una herramienta util, pero conviene reservarlos para metadatos genuinos. Si algo es logica de negocio, debe vivir en un metodo, no en un atributo. Si es configuracion repetida o transversal que varias herramientas consumen, entonces el atributo es el sitio correcto.
Los atributos mas utiles son los que documentan el como se usa una clase por el resto del sistema: que valida, que ruta expone, a que tabla se mapea, que pruebas ejecuta. Mantener esa disciplina hace que la lectura de una clase siga siendo directa aunque este rodeada de metadatos.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, PHP es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de PHP
Explora más contenido relacionado con PHP y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender los atributos nativos de PHP 8, declarar tus propios atributos, leerlos por reflexion y aplicarlos a validacion, routing y mapeo de entidades.