Atributos foram introduzidos no PHP 8 como uma forma nativa de adicionar metadados a classes, métodos, propriedades e outros elementos. Diferente de comentários em docblock, atributos são estruturados e podem ser lidos programaticamente via reflexão, permitindo comportamentos dinâmicos e validações. Nesta aula, você aprenderá a sintaxe, como acessá-los e aplicá-los em cenários reais.

Atributos substituem a necessidade de anotações em docblock para muitos casos, trazendo tipagem forte e integração com o sistema de tipos do PHP. Eles são especialmente úteis em frameworks, ORMs e bibliotecas de validação.

Sintaxe #[ ]

A sintaxe de atributos usa colchetes duplos: #[NomeDoAtributo]. Eles podem ser aplicados a classes, métodos, propriedades, constantes, parâmetros e closures. Atributos podem aceitar argumentos, que podem ser constantes, expressões simples ou arrays.

Exemplo básico:

#[\Attribute]
class MyAttribute {
    public function __construct(public string $name) {}
}

#[MyAttribute('example')]
class MyClass {}

Você pode usar múltiplos atributos no mesmo elemento, separados por vírgula ou em blocos separados. Também é possível usar atributos repetíveis marcando a classe do atributo com #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)].

Lendo via reflexão

A leitura de atributos é feita através da API de reflexão do PHP, como ReflectionClass, ReflectionMethod, ReflectionProperty, etc. O método getAttributes() retorna um array de objetos ReflectionAttribute.

$reflection = new ReflectionClass(MyClass::class);
$attributes = $reflection->getAttributes();

foreach ($attributes as $attribute) {
    echo $attribute->getName() . "\n";
    var_dump($attribute->getArguments());
}

Para instanciar o atributo, use $attribute->newInstance(), que chamará o construtor com os argumentos fornecidos. Isso permite acesso direto aos valores.

Casos de uso

Atributos são amplamente usados em:

  • Mapeamento ORM: Como no Doctrine, para definir entidades e colunas.
  • Validação: Validar dados de entrada com regras como #[NotEmpty], #[Email].
  • Roteamento: Em frameworks como Symfony, para definir rotas em controllers.
  • Injeção de dependência: Marcar serviços para autowiring.
  • Serialização: Controlar como propriedades são serializadas (ex: JsonSerializable).

Exemplo de validação personalizada:

#[\Attribute(\Attribute::TARGET_PROPERTY)]
class NotBlank {
    public function __construct(public string $message = 'Value cannot be blank') {}
}

class User {
    #[NotBlank(message: 'Name is required')]
    public string $name;
}

// Validação via reflexão
function validate(object $object): void {
    $reflection = new ReflectionClass($object);
    foreach ($reflection->getProperties() as $property) {
        $attributes = $property->getAttributes(NotBlank::class);
        if (!empty($attributes) && empty($property->getValue($object))) {
            throw new \InvalidArgumentException($attributes[0]->newInstance()->message);
        }
    }
}

vs anotações em docblock

Anotações em docblock (@annotation) são strings de comentário que exigem análise manual (ex: regex) ou bibliotecas como Doctrine Annotations. Elas são frágeis, sem verificação de tipo e sem suporte nativo da linguagem.

Atributos, por outro lado, são parte da sintaxe do PHP, com verificação de tipo, autocomplete em IDEs e performance melhor por não dependerem de parsing de comentários. Além disso, atributos podem ser alvo de verificação estática (como PHPStan).

Comparação prática:

// Docblock (antes do PHP 8)
/**
 * @Route("/api/users", methods={"GET"})
 */
class UserController {}

// Atributo (PHP 8+)
#[Route('/api/users', methods: ['GET'])]
class UserController {}

Atributos são a evolução natural e recomendada para metadados em PHP moderno.

Boas práticas

  • Prefira atributos a docblock para novos projetos.
  • Defina atributos como classes dedicadas, com construtor tipado.
  • Use constantes e expressões simples nos argumentos para evitar complexidade.
  • Documente seus atributos com docblock para outros desenvolvedores.

Referências

Exercícios

  1. Crie um atributo chamado `MinLength` que aceite um parâmetro `int $length` e uma mensagem opcional. Aplique-o a uma propriedade e escreva uma função de validação que use reflexão para verificar se o valor da propriedade tem pelo menos o comprimento especificado.

    ✓ Resposta:
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    class MinLength {
        public function __construct(
            public int $length,
            public string $message = 'Value is too short'
        ) {}
    }
    
    class User {
        #[MinLength(3, message: 'Name must have at least 3 characters')]
        public string $name;
    }
    
    function validateMinLength(object $object): void {
        $reflection = new ReflectionClass($object);
        foreach ($reflection->getProperties() as $property) {
            $attrs = $property->getAttributes(MinLength::class);
            if (!empty($attrs)) {
                $minLength = $attrs[0]->newInstance();
                $value = $property->getValue($object);
                if (strlen($value) < $minLength->length) {
                    throw new \InvalidArgumentException($minLength->message);
                }
            }
        }
    }
    
  2. Use reflexão para listar todos os atributos de uma classe e seus argumentos, imprimindo o nome do atributo e os argumentos em formato legível.

    ✓ Resposta:
    function listAttributes(string $class): void {
        $reflection = new ReflectionClass($class);
        $attributes = $reflection->getAttributes();
        foreach ($attributes as $attr) {
            echo $attr->getName() . ": ";
            print_r($attr->getArguments());
        }
    }
    
  3. Implemente um atributo `#[Route]` que aceite `string $path` e `array $methods`. Aplique a métodos de uma classe e crie um roteador simples que mapeie caminhos para métodos.

    ✓ Resposta:
    #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
    class Route {
        public function __construct(
            public string $path,
            public array $methods = ['GET']
        ) {}
    }
    
    class UserController {
        #[Route('/users', methods: ['GET'])]
        public function list(): void { echo "list users\n"; }
    
        #[Route('/users', methods: ['POST'])]
        public function create(): void { echo "create user\n"; }
    }
    
    function router(string $class, string $uri, string $method): void {
        $reflection = new ReflectionClass($class);
        foreach ($reflection->getMethods() as $methodRef) {
            $attrs = $methodRef->getAttributes(Route::class);
            foreach ($attrs as $attr) {
                $route = $attr->newInstance();
                if ($route->path === $uri && in_array($method, $route->methods)) {
                    $instance = $reflection->newInstanceWithoutConstructor();
                    $methodRef->invoke($instance);
                    return;
                }
            }
        }
        throw new \RuntimeException("No route found");
    }
    
  4. Converta um docblock annotation `@Deprecated` em um atributo nativo. Crie o atributo e mostre como verificar se um método está marcado como deprecated.

    ✓ Resposta:
    #[\Attribute(\Attribute::TARGET_METHOD)]
    class Deprecated {
        public function __construct(
            public string $message = 'This method is deprecated'
        ) {}
    }
    
    class OldClass {
        #[Deprecated('Use newMethod instead')]
        public function oldMethod(): void {}
    }
    
    function checkDeprecated(string $class, string $method): void {
        $reflection = new ReflectionMethod($class, $method);
        $attrs = $reflection->getAttributes(Deprecated::class);
        if (!empty($attrs)) {
            $deprecated = $attrs[0]->newInstance();
            trigger_error($deprecated->message, E_USER_DEPRECATED);
        }
    }
    
  5. Crie um atributo `#[JsonExclude]` para propriedades que não devem ser serializadas em JSON. Implemente uma função de serialização que ignore propriedades com esse atributo.

    ✓ Resposta:
    #[\Attribute(\Attribute::TARGET_PROPERTY)]
    class JsonExclude {}
    
    class User {
        public string $name;
        #[JsonExclude]
        public string $password;
    }
    
    function toJson(object $object): string {
        $reflection = new ReflectionClass($object);
        $data = [];
        foreach ($reflection->getProperties() as $property) {
            if (!empty($property->getAttributes(JsonExclude::class))) {
                continue;
            }
            $property->setAccessible(true);
            $data[$property->getName()] = $property->getValue($object);
        }
        return json_encode($data);
    }