Atributos nativos: metadados no código

1. Introdução aos Atributos no PHP 8.0

Atributos nativos foram introduzidos no PHP 8.0 como uma forma estruturada e tipada de adicionar metadados ao código. Antes deles, desenvolvedores dependiam de docblocks (PHPDoc) para anotar classes, métodos e propriedades — uma abordagem baseada em strings, sem validação sintática ou semântica pela engine do PHP.

A principal diferença entre atributos nativos e docblocks é que atributos são construções da linguagem, não comentários. Enquanto docblocks são ignorados pelo interpretador (a menos que uma biblioteca os leia via reflexão de texto), atributos são parseados e validados pelo PHP, permitindo que ferramentas como IDEs, analisadores estáticos e frameworks os consumam de forma confiável.

A sintaxe básica é simples: #[Atributo] para um atributo sem argumentos, ou #[Atributo(argumentos)] para configurá-lo com parâmetros.

#[Route('/api/users')]
class UserController {
    #[NotEmpty]
    public string $name;
}

2. Estrutura e Declaração de Atributos

Para criar um atributo customizado, você define uma classe comum e a anota com o atributo #[Attribute]. Opcionalmente, especifica os alvos permitidos e se o atributo pode ser repetido no mesmo elemento.

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]
class NotEmpty {
    public function __construct(
        public string $message = 'Este campo não pode estar vazio'
    ) {}
}

Os alvos disponíveis incluem:
- TARGET_CLASS — classes, interfaces, traits, enums
- TARGET_METHOD — métodos
- TARGET_PROPERTY — propriedades
- TARGET_FUNCTION — funções
- TARGET_PARAMETER — parâmetros
- TARGET_ALL — qualquer elemento

Para permitir múltiplas instâncias do mesmo atributo, use Attribute::IS_REPEATABLE:

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class ValidationRule {
    public function __construct(public string $rule) {}
}

class User {
    #[ValidationRule('required')]
    #[ValidationRule('min:3')]
    public string $username;
}

3. Atributos Nativos do PHP

O PHP fornece alguns atributos nativos úteis:

  • #[Attribute] — metaclasse para definir atributos customizados, como vimos acima.
  • #[ReturnTypeWillChange] — usado para suprimir avisos de incompatibilidade de tipo de retorno ao implementar interfaces legadas (como ArrayAccess).
  • #[Deprecated] (PHP 8.3+) — marca elementos como obsoletos, gerando avisos em tempo de compilação.
  • #[Override] (PHP 8.3+) — garante que um método está sobrescrevendo um método de uma classe pai ou interface.
  • #[SensitiveParameter] — oculta valores de parâmetros em stack traces, útil para senhas e tokens.
class LegacyCollection implements \ArrayAccess {
    #[ReturnTypeWillChange]
    public function offsetGet(mixed $offset): mixed {
        return $this->data[$offset] ?? null;
    }
}

#[Deprecated('Use novaClasse em vez disso')]
class OldService {}

class BaseController {
    public function index(): void {}
}

class UserController extends BaseController {
    #[Override]
    public function index(): void {
        // Se BaseController::index não existir, erro de compilação
    }
}

function login(#[SensitiveParameter] string $password): void {
    throw new \RuntimeException('Erro simulado');
}

4. Leitura e Reflexão de Atributos em Tempo de Execução

Atributos são lidos via reflexão usando a classe ReflectionAttribute. O processo envolve:
1. Obter um objeto de reflexão (ReflectionClass, ReflectionMethod, ReflectionProperty, etc.)
2. Chamar getAttributes() para obter um array de ReflectionAttribute
3. Usar newInstance() para instanciar o atributo com seus argumentos

class Product {
    #[NotEmpty]
    #[Email]
    public string $email;
}

$reflection = new ReflectionProperty(Product::class, 'email');
$attributes = $reflection->getAttributes();

foreach ($attributes as $attribute) {
    $instance = $attribute->newInstance();
    echo get_class($instance) . "\n"; // NotEmpty, Email
}

Você pode filtrar por nome de classe específico:

$emailAttributes = $reflection->getAttributes(Email::class);
if (!empty($emailAttributes)) {
    $emailAttr = $emailAttributes[0]->newInstance();
}

5. Casos de Uso Práticos com Atributos Customizados

Validação de dados

#[Attribute(Attribute::TARGET_PROPERTY)]
class Email {
    public function __construct(public string $message = 'Email inválido') {}
}

class UserRegistration {
    #[NotEmpty('Nome é obrigatório')]
    public string $name;

    #[Email('Forneça um email válido')]
    public string $email;
}

function validate(object $obj): array {
    $errors = [];
    $reflection = new ReflectionClass($obj);
    foreach ($reflection->getProperties() as $prop) {
        foreach ($prop->getAttributes() as $attr) {
            $validator = $attr->newInstance();
            $value = $prop->getValue($obj);
            if ($validator instanceof NotEmpty && empty($value)) {
                $errors[$prop->getName()] = $validator->message;
            }
            if ($validator instanceof Email && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
                $errors[$prop->getName()] = $validator->message;
            }
        }
    }
    return $errors;
}

Roteamento em frameworks

#[Attribute(Attribute::TARGET_METHOD)]
class Route {
    public function __construct(
        public string $path,
        public string $method = 'GET'
    ) {}
}

class ApiController {
    #[Route('/users', 'GET')]
    public function listUsers(): array {
        return ['users' => []];
    }

    #[Route('/users', 'POST')]
    public function createUser(): array {
        return ['status' => 'created'];
    }
}

Serialização

#[Attribute(Attribute::TARGET_PROPERTY)]
class JsonIgnore {}

class User {
    public string $name;
    public string $email;

    #[JsonIgnore]
    public string $password;
}

function toJson(object $obj): string {
    $data = [];
    $reflection = new ReflectionClass($obj);
    foreach ($reflection->getProperties() as $prop) {
        if (!empty($prop->getAttributes(JsonIgnore::class))) {
            continue;
        }
        $data[$prop->getName()] = $prop->getValue($obj);
    }
    return json_encode($data);
}

6. Boas Práticas e Limitações

Quando usar atributos vs. interfaces vs. traits:
- Use interfaces para contratos comportamentais (o que uma classe faz).
- Use traits para reúso de implementação concreta.
- Use atributos para metadados declarativos que não alteram o comportamento da classe, apenas o descrevem.

Performance: Atributos são lidos exclusivamente via reflexão, que é um processo mais lento que chamadas diretas. Para aplicações de alto desempenho, considere cachear os resultados da leitura de atributos (ex.: em um container de injeção de dependência).

Limitações:
- Atributos não podem ser aplicados a closures, tipos escalares ou expressões.
- Não é possível usar atributos em variáveis locais ou parâmetros de tipos primitivos (apenas parâmetros de funções/métodos).
- A leitura de atributos requer reflexão, o que pode ser verboso.

Versionamento: Atributos foram introduzidos no PHP 8.0. #[Override] e #[Deprecated] chegaram no PHP 8.3. Mantenha compatibilidade com versões anteriores usando polyfills ou condicionais.

7. Comparação com Docblocks e Anotações de Bibliotecas

Vantagens dos atributos nativos:
- Tipagem forte: Argumentos são validados pelo PHP (tipos, valores padrão).
- Suporte de IDE: Autocomplete, refatoração, navegação.
- Performance: Parseados uma vez pela engine, sem necessidade de ler docblocks como strings.
- Semântica clara: Não há ambiguidade entre comentários e metadados.

Migração de anotações Doctrine para atributos nativos:

Código legado com docblocks:

/**
 * @ORM\Entity(repositoryClass="UserRepository")
 * @ORM\Table(name="users")
 */
class User {
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private int $id;
}

Código moderno com atributos nativos:

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User {
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private int $id;
}

A migração é direta e melhora a legibilidade, além de permitir que IDEs ofereçam melhor suporte.

8. Conclusão e Próximos Passos

Atributos nativos representam um avanço significativo na forma como metadados são declarados em PHP. Eles substituem docblocks com uma sintaxe formal, tipada e verificável pela engine, melhorando a manutenibilidade, a segurança e a integração com ferramentas.

Combinados com outros recursos modernos do PHP (como readonly, tipos union, enums e exceções tipadas), atributos permitem construir sistemas mais expressivos e auto-documentados. Frameworks como Symfony e Laravel já adotaram atributos nativos para roteamento, validação e injeção de dependência.

Para aprofundar, explore a documentação oficial, as RFCs originais e os tutoriais práticos listados nas referências. Experimente migrar pequenos trechos de código com docblocks para atributos e sinta a diferença na experiência de desenvolvimento.

Referências