PHP Atributos

Um atributo é uma etiqueta que você cola num pedaço do código (uma classe, um método, um parâmetro) para dizer algo sobre ele, sem mudar o que ele faz: "este método substitui o da classe-mãe", "esta função está obsoleta", "este parâmetro é uma senha, não mostre". Quem lê a etiqueta pode ser o próprio PHP, uma ferramenta, ou o seu código. A sintaxe é #[...].

Você vai aprender
  • A sintaxe #[...] e onde ela pode ser usada. PHP 8.0+
  • #[\Override]: pegar erro de digitação em métodos herdados. PHP 8.3+
  • #[\Deprecated]: marcar suas funções como obsoletas. PHP 8.4+
  • #[\SensitiveParameter]: esconder senhas dos rastros de erro. PHP 8.2+
  • #[\NoDiscard]: avisar quando alguém ignora o resultado. PHP 8.5+
  • A criar o seu próprio atributo e lê-lo com Reflection.

Esta aula usa herança, construtores e namespaces.


A ideia: etiquetas na mercadoria

Na lojinha da Ana, uma caixa pode ter etiquetas: "FRÁGIL", "ESTE LADO PARA CIMA", "VENCE EM 10/2026". A etiqueta não muda o que está dentro da caixa; ela informa quem manuseia. O entregador lê "FRÁGIL" e muda o comportamento dele.

Atributos são etiquetas no código. São metadados: dados sobre o código, e não dados do programa. Antes do PHP 8, as bibliotecas faziam isso com comentários especiais (/** @Route("/produtos") */), lidos como texto. Os atributos são a versão oficial: o PHP confere a sintaxe e entrega os dados prontos.


A sintaxe #[...] PHP 8.0+

<?php
#[MinhaEtiqueta]                          // sem argumentos
#[Rota('/produtos', metodo: 'POST')]      // com argumentos (até nomeados)
#[Etiqueta1, Etiqueta2('x')]              // duas de uma vez
class Exemplo
{
    #[Coluna('preco')]                    // numa propriedade
    public float $preco = 0;

    #[\Override]                          // num método
    public function __toString(): string { return "ex"; }

    public function entrar(#[\SensitiveParameter] string $senha) {}   // num parâmetro
}
#[ … ]
Abre e fecha o atributo. Fica logo antes do que ele etiqueta, normalmente na linha de cima.
Rota
O nome do atributo é o nome de uma classe. Segue as regras de namespaces: #[\Override] com barra é a classe Override do PHP; sem use nem barra, dentro de um namespace, o PHP procuraria SeuNamespace\Override.
('/produtos', metodo: 'POST')
Argumentos, como numa chamada de função, inclusive nomeados. Só valores constantes: textos, números, arrays, constantes, new de objetos simples. Nada de variáveis.

Podem receber atributos: classes (e interfaces, traits, enums), funções, métodos, propriedades, constantes de classe, parâmetros, casos de enum e funções anônimas.

Por que #[?

Em PHP, # começa um comentário de uma linha. Até o PHP 7.4, #[Rota('/x')] era só um comentário e era ignorado. Isso foi de propósito: uma biblioteca pode usar atributos e continuar rodando (sem eles) em PHP antigo. O lado ruim: no PHP 8, um comentário que começa com #[ deixa de ser comentário.

Um atributo, sozinho, não faz nada. O que dá efeito é alguém ler a etiqueta. Os atributos nativos (da barra invertida, como #[\Override]) são lidos pelo próprio PHP. Vamos a eles.


#[\Override]: "estou substituindo um método" PHP 8.3+

A classe Pix quer substituir o método processar da classe-mãe Pagamento. Mas a Ana digitou procesar, com um s só:

<?php
class Pagamento
{
    public function processar(float $valor): string
    {
        return "Pagamento de $valor";
    }
}

class Pix extends Pagamento
{
    public function procesar(float $valor): string
    {
        return "Pix de $valor";
    }
}

echo (new Pix())->processar(10), "\n";
$ php sem-override.php
Pagamento de 10

Nenhum erro, e o resultado está errado: para o PHP, procesar é só um método novo, e o processar chamado foi o da mãe. Esse tipo de engano pode ficar meses escondido. Com o atributo, a Ana declara a intenção:

<?php
class Pagamento
{
    public function processar(float $valor): string
    {
        return "Pagamento de $valor";
    }
}

class Pix extends Pagamento
{
    #[\Override]
    public function procesar(float $valor): string
    {
        return "Pix de $valor";
    }
}

echo (new Pix())->processar(10), "\n";
$ php override-erro.php

Fatal error: Pix::procesar() has #[\Override] attribute, but no matching parent method exists in /home/ana/attr/override-erro.php on line 13
Stack trace:
#0 {main}

"Pix::procesar() tem o atributo #[\Override], mas não existe um método correspondente na classe-mãe". O erro aparece antes de qualquer linha rodar, apontando o método. Corrigindo o nome:

$ php override.php
Pix de 10

O #[\Override] também protege contra o contrário: se um dia alguém renomear o método na classe-mãe, todas as filhas marcadas dão erro na hora, em vez de pararem de funcionar em silêncio. Vale para métodos que vêm de classes-mãe e de interfaces; no PHP 8.5, também para propriedades. Em funções soltas, não faz sentido:

$ php override-interno.php       « #[Override] function f() {}

Fatal error: Attribute "Override" cannot target function (allowed targets: method, property) in /home/ana/attr/override-interno.php on line 3
Stack trace:
#0 {main}

Missão: a classe Boleto deveria substituir processar, mas a saída mostra "Pagamento". Coloque #[\Override] em cima do método da Boleto, rode para ver o erro apontar o problema e conserte o nome. A saída deve ser Boleto de R$ 10.

<?php
class Pagamento
{
    public function processar(float $valor): string
    {
        return "Pagamento de R$ $valor";
    }
}

class Boleto extends Pagamento
{
    public function procesar(float $valor): string
    {
        return "Boleto de R$ $valor";
    }
}

echo (new Boleto())->processar(10), "\n";
<?php
class Pagamento
{
    public function processar(float $valor): string
    {
        return "Pagamento de R$ $valor";
    }
}

class Boleto extends Pagamento
{
    #[\Override]
    public function processar(float $valor): string
    {
        return "Boleto de R$ $valor";
    }
}

echo (new Boleto())->processar(10), "\n";

#[\Deprecated]: "não use mais" PHP 8.4+

O PHP sempre avisou quando você usa uma função dele que vai sumir (Deprecated: …). Desde o 8.4, você pode fazer o mesmo com as suas funções, métodos e constantes de classe. Útil quando outras pessoas usam o seu código: você cria o substituto, marca o antigo, e elas têm tempo de trocar.

<?php
#[\Deprecated(message: "use calcularFrete()", since: "2.0")]
function frete(float $peso): float
{
    return calcularFrete($peso);
}

function calcularFrete(float $peso): float
{
    return 10 + $peso * 2;
}

class Loja
{
    #[\Deprecated("use Loja::TAXA_NOVA")]
    public const TAXA = 0.05;
    public const TAXA_NOVA = 0.06;

    #[\Deprecated]
    public function totalAntigo(): int { return 0; }
}

echo frete(3), "\n";
echo Loja::TAXA, "\n";
echo (new Loja())->totalAntigo(), "\n";
echo "fim\n";
message: "…"
O que fazer em vez disso. Opcional.
since: "2.0"
Desde qual versão do seu código está obsoleto. Opcional.
#[\Deprecated]
Sem argumentos também vale.
$ php deprecated.php

Deprecated: Function frete() is deprecated since 2.0, use calcularFrete() in /home/ana/attr/deprecated.php on line 23
16

Deprecated: Constant Loja::TAXA is deprecated, use Loja::TAXA_NOVA in /home/ana/attr/deprecated.php on line 24
0.05

Deprecated: Method Loja::totalAntigo() is deprecated in /home/ana/attr/deprecated.php on line 25
0
fim

Repare: tudo continuou funcionando (16, 0.05, 0, fim). Deprecated é um aviso, não um erro. A linha indicada é a do uso (23, 24, 25), não a da declaração, para quem usa saber onde trocar. Os avisos aparecem se o error_reporting incluir E_DEPRECATED (o padrão em desenvolvimento; veja Tipos de Erro).


#[\SensitiveParameter]: esconda a senha PHP 8.2+

Quando uma exceção não é tratada, o PHP mostra o stack trace com os valores dos argumentos. Em produção, esse rastro vai para os arquivos de log. Veja o problema:

<?php
function entrar(string $usuario, string $senha): void
{
    throw new Exception("Usuário ou senha inválidos");
}

entrar("ana", "minhaSenha123");
$ php sem-sensivel.php

Fatal error: Uncaught Exception: Usuário ou senha inválidos in /home/ana/attr/sem-sensivel.php:4
Stack trace:
#0 /home/ana/attr/sem-sensivel.php(7): entrar('ana', 'minhaSenha123')
#1 {main}
  thrown in /home/ana/attr/sem-sensivel.php on line 4

A senha está ali, em texto puro, para quem ler o log. Com o atributo no parâmetro:

<?php
function entrar(string $usuario, #[\SensitiveParameter] string $senha): void
{
    throw new Exception("Usuário ou senha inválidos");
}

entrar("ana", "minhaSenha123");
$ php sensivel.php

Fatal error: Uncaught Exception: Usuário ou senha inválidos in /home/ana/attr/sensivel.php:4
Stack trace:
#0 /home/ana/attr/sensivel.php(7): entrar('ana', Object(SensitiveParameterValue))
#1 {main}
  thrown in /home/ana/attr/sensivel.php on line 4

No lugar da senha, aparece Object(SensitiveParameterValue), uma "caixa fechada". Use em todo parâmetro que recebe senha, token, número de cartão ou chave de API. As próprias funções do PHP, como password_hash(), já vêm marcadas assim (veja Senhas).


#[\NoDiscard]: "não ignore o resultado" PHP 8.5+

Algumas funções devolvem uma informação que não pode ser jogada fora: "deu certo?", "o valor novo". Chamar salvar(…) sem olhar se salvou é um erro comum. O PHP 8.5 permite marcar a função para que ignorar o resultado gere um aviso:

<?php
#[\NoDiscard("o resultado diz se o arquivo foi salvo")]
function salvar(string $arquivo, string $texto): bool
{
    return file_put_contents($arquivo, $texto) !== false;
}

salvar('/tmp/nota.txt', 'oi');
echo "1. ignorei o resultado\n";

$ok = salvar('/tmp/nota.txt', 'oi');
echo "2. guardei: ", var_export($ok, true), "\n";

(void) salvar('/tmp/nota.txt', 'oi');
echo "3. ignorei de propósito\n";
$ php nodiscard.php

Warning: The return value of function salvar() should either be used or intentionally ignored by casting it as (void), o resultado diz se o arquivo foi salvo in /home/ana/attr/nodiscard.php on line 8
1. ignorei o resultado
2. guardei: true
3. ignorei de propósito

A mensagem: "o valor de retorno de salvar() deve ser usado ou ignorado de propósito com (void)", seguida do texto que a Ana escreveu no atributo. Só a chamada da linha 8 avisou:

ChamadaAviso?
salvar(…); (resultado jogado fora)Sim: Warning
$ok = salvar(…); (resultado usado)Não
(void) salvar(…); (ignorado de propósito)Não. O "cast" (void) também é novo no PHP 8.5 e só serve para isso

Experimente: rode e veja o Warning. Depois tire o atributo #[\NoDiscard] e rode de novo; em seguida, recoloque-o e troque a linha reservar("Caneca"); por $ok = reservar("Caneca");.

<?php
#[\NoDiscard("confira se a reserva deu certo")]
function reservar(string $produto): bool
{
    return $produto !== "Boné";   // o boné esgotou
}

reservar("Caneca");
echo "reserva enviada\n";

Criando o seu atributo

Agora o outro lado: o seu código lendo etiquetas. Frameworks usam isso para rotas (#[Route('/produtos')]), validação (#[Email]), banco de dados (#[Column]) e testes (#[Test], no PHPUnit). Vamos montar um mini-roteador.

Passo 1: a classe do atributo

Um atributo é uma classe comum, marcada com o atributo nativo #[Attribute]:

<?php
#[Attribute(Attribute::TARGET_METHOD)]
class Rota
{
    public function __construct(
        public string $caminho,
        public string $metodo = 'GET',
    ) {}
}
#[Attribute]
"Esta classe pode ser usada como atributo." Sem isso, o PHP recusa (veja "Quando dá errado").
Attribute::TARGET_METHOD
Onde ela pode ser colada: só em métodos. Outras opções: TARGET_CLASS, TARGET_PROPERTY, TARGET_FUNCTION, TARGET_PARAMETER, TARGET_ALL (padrão). Combine com |, e acrescente Attribute::IS_REPEATABLE para permitir colar a mesma etiqueta duas vezes.
__construct(…)
Os argumentos do atributo (#[Rota('/produtos', metodo: 'POST')]) vão para este construtor.

Passo 2: colar as etiquetas

class ProdutoController
{
    #[Rota('/produtos')]
    public function listar(): string
    {
        return "lista de produtos";
    }

    #[Rota('/produtos', metodo: 'POST')]
    public function criar(): string
    {
        return "produto criado";
    }

    public function ajudante(): void {}
}

Passo 3: ler com Reflection

Reflection ("reflexão") é a parte do PHP que deixa um programa examinar o próprio código: quais métodos uma classe tem, quais parâmetros, quais atributos. Primeiro, o básico, num método só:

$metodo = new ReflectionMethod(ProdutoController::class, 'criar');
$atributos = $metodo->getAttributes();

echo count($atributos), " atributo(s)\n";
$a = $atributos[0];
echo "Nome: ", $a->getName(), "\n";
echo "Argumentos: ";
var_dump($a->getArguments());
$rota = $a->newInstance();
var_dump($rota);
$ php ler-basico.php
1 atributo(s)
Nome: Rota
Argumentos: array(2) {
  [0]=>
  string(9) "/produtos"
  ["metodo"]=>
  string(4) "POST"
}
object(Rota)#3 (2) {
  ["caminho"]=>
  string(9) "/produtos"
  ["metodo"]=>
  string(4) "POST"
}
MétodoDevolve
new ReflectionMethod(Classe::class, 'metodo')Um objeto que descreve o método
->getAttributes()Lista de ReflectionAttribute, um por etiqueta. getAttributes(Rota::class) filtra só as Rota
->getName()O nome completo da classe do atributo
->getArguments()Os argumentos, como array (nomeados com a chave do nome)
->newInstance()Cria o objeto Rota de verdade, chamando o construtor. É aqui que o PHP confere se a classe existe e se o alvo é permitido

Agora o roteador completo, que percorre todos os métodos da classe:

$classe = new ReflectionClass(ProdutoController::class);

foreach ($classe->getMethods() as $metodo) {
    foreach ($metodo->getAttributes(Rota::class) as $atributo) {
        $rota = $atributo->newInstance();
        echo str_pad($rota->metodo, 5), $rota->caminho, " -> ", $metodo->getName(), "()\n";
    }
}
$ php rotas.php
GET  /produtos -> listar()
POST /produtos -> criar()

O método ajudante(), sem etiqueta, ficou de fora. Um framework faria o passo seguinte: comparar o caminho e o método da requisição ($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']) com essa tabela e chamar o método certo. É o que você vai fazer, sem atributos, em Criar uma API JSON.


Atributo ou outra coisa? Qual usar

NecessidadeUse
Pegar erro de nome em método herdado#[\Override] em todo método que substitui outro
Avisar que algo vai sumir#[\Deprecated] (no PHP < 8.4: comentário @deprecated + trigger_error(…, E_USER_DEPRECATED))
Proteger senhas nos logs#[\SensitiveParameter]
Obrigar a conferir o resultado#[\NoDiscard], ou lançar uma exceção quando der errado (mais forte: não dá para ignorar)
Configuração lida pelo seu código ou frameworkAtributo próprio + Reflection
Documentação para pessoasComentários /** … */
Comportamento do programaCódigo normal. Atributos não executam nada sozinhos

Outros atributos nativos que você pode encontrar: #[\AllowDynamicProperties] (8.2: permite criar propriedades não declaradas, veja Novidades) e #[\ReturnTypeWillChange] (silencia avisos de compatibilidade em código antigo).


Quando dá errado

has #[\Override] attribute, but no matching parent method exists

O que significa: o método marcado não substitui nada: nome digitado errado, classe-mãe errada, ou o método da mãe foi renomeado/removido.

Como consertar: confira o nome na classe-mãe (ou na interface) e o extends/implements.

Attribute class "Rotta" not found
$ php inexistente.php
lista
Nome lido: Rotta

Fatal error: Uncaught Error: Attribute class "Rotta" not found in /home/ana/attr/inexistente.php:11
Stack trace:
#0 /home/ana/attr/inexistente.php(11): ReflectionAttribute->newInstance()
#1 {main}
  thrown in /home/ana/attr/inexistente.php on line 11

O que significa: o atributo tem um nome que não é classe nenhuma. Repare que o programa rodou normalmente (lista) e até leu o nome: o PHP não confere atributos próprios até alguém chamar newInstance(). Por isso um erro de digitação num atributo pode passar despercebido.

Como consertar: corrija o nome; confira o use do namespace do atributo.

Attempting to use non-attribute class "Rota" as attribute
$ php nao-atributo.php

Fatal error: Uncaught Error: Attempting to use non-attribute class "Rota" as attribute in /home/ana/attr/nao-atributo.php:14
Stack trace:
#0 /home/ana/attr/nao-atributo.php(14): ReflectionAttribute->newInstance()
#1 {main}
  thrown in /home/ana/attr/nao-atributo.php on line 14

O que significa: a classe existe, mas falta o #[Attribute] em cima dela.

Como consertar: acrescente #[Attribute] (com o alvo, se quiser) acima de class Rota.

Attribute "Rota" cannot target class (allowed targets: method)
$ php alvo-errado.php
Lido: Rota

Fatal error: Uncaught Error: Attribute "Rota" cannot target class (allowed targets: method) in /home/ana/attr/alvo-errado.php:13
Stack trace:
#0 /home/ana/attr/alvo-errado.php(13): ReflectionAttribute->newInstance()
#1 {main}
  thrown in /home/ana/attr/alvo-errado.php on line 13

O que significa: a etiqueta foi colada numa classe, mas a Rota só aceita métodos (TARGET_METHOD).

Como consertar: mova a etiqueta para um método, ou amplie o alvo: #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)].

Attribute "Rota" must not be repeated
$ php repetido.php

Fatal error: Uncaught Error: Attribute "Rota" must not be repeated in /home/ana/attr/repetido.php:16
Stack trace:
#0 /home/ana/attr/repetido.php(16): ReflectionAttribute->newInstance()
#1 {main}
  thrown in /home/ana/attr/repetido.php on line 16

O que significa: duas etiquetas #[Rota] no mesmo método, e a classe não permite repetição.

Como consertar: #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)].


Referência

Atributo / itemPara quêDesde
#[Attribute]Marca uma classe como atributo8.0
#[\SensitiveParameter]Esconde o valor do parâmetro nos rastros8.2
#[\AllowDynamicProperties]Permite propriedades dinâmicas na classe8.2
#[\Override]Garante que o método substitui outro8.3
#[\Deprecated]Marca função, método ou constante de classe como obsoleta8.4
#[\NoDiscard] e (void)Avisa quando o retorno é ignorado8.5
ReflectionClass, ReflectionMethod, ReflectionProperty, ReflectionFunctionExaminam o código; todos têm getAttributes()5 / 8.0
ReflectionAttribute::getName(), getArguments(), newInstance()Nome, argumentos, objeto do atributo8.0

Exercícios

1. Proteger a senha

Complete o atributo que esconde o valor do parâmetro $senha nos rastros de erro:

function login(string $email, #[] string $senha)

2. Ignorar de propósito

A função salvar() tem #[\NoDiscard]. Complete para chamá-la ignorando o resultado sem aviso:

salvar($arquivo, $texto);

3. Marcar a classe

Complete a linha que transforma a classe Coluna num atributo:


class Coluna { … }

4. Um atributo próprio com nome errado (#[Rotta], classe inexistente). Quando o PHP reclama?

5. O que o PHP 7.4 faz com a linha #[\Override]?

6. Uma função marcada com #[\Deprecated] é chamada. O que acontece?

Missão final: o roteador lista só as rotas dos métodos etiquetados. Cole etiquetas Rota em mais dois métodos: formulario() deve responder a GET em /produtos/novo, e criar() a POST em /produtos (use o argumento nomeado metodo:). O ajudante() continua sem rota.

<?php
#[Attribute(Attribute::TARGET_METHOD)]
class Rota
{
    public function __construct(
        public string $caminho,
        public string $metodo = 'GET',
    ) {}
}

class ProdutoController
{
    #[Rota('/produtos')]
    public function listar(): string { return "lista"; }

    public function formulario(): string { return "formulário"; }

    public function criar(): string { return "criado"; }

    public function ajudante(): void {}
}

$classe = new ReflectionClass(ProdutoController::class);
foreach ($classe->getMethods() as $metodo) {
    foreach ($metodo->getAttributes(Rota::class) as $atributo) {
        $rota = $atributo->newInstance();
        echo str_pad($rota->metodo, 5), $rota->caminho, " -> ", $metodo->getName(), "()\n";
    }
}
<?php
#[Attribute(Attribute::TARGET_METHOD)]
class Rota
{
    public function __construct(
        public string $caminho,
        public string $metodo = 'GET',
    ) {}
}

class ProdutoController
{
    #[Rota('/produtos')]
    public function listar(): string { return "lista"; }

    #[Rota('/produtos/novo')]
    public function formulario(): string { return "formulário"; }

    #[Rota('/produtos', metodo: 'POST')]
    public function criar(): string { return "criado"; }

    public function ajudante(): void {}
}

$classe = new ReflectionClass(ProdutoController::class);
foreach ($classe->getMethods() as $metodo) {
    foreach ($metodo->getAttributes(Rota::class) as $atributo) {
        $rota = $atributo->newInstance();
        echo str_pad($rota->metodo, 5), $rota->caminho, " -> ", $metodo->getName(), "()\n";
    }
}

Resumo

  • Atributos são etiquetas #[Nome(args)] antes de classes, métodos, propriedades, parâmetros… Não executam nada: alguém precisa lê-las.
  • Nativos lidos pelo PHP: #[\Override] (8.3, pega erro de nome), #[\Deprecated] (8.4, aviso de obsoleto), #[\SensitiveParameter] (8.2, esconde senhas), #[\NoDiscard] (8.5, aviso se o retorno for ignorado; (void) para ignorar de propósito).
  • Atributo próprio = classe com #[Attribute(alvos)]; os argumentos vão para o construtor.
  • Leitura: ReflectionClass/ReflectionMethod → getAttributes() → getName(), getArguments(), newInstance().
  • Erros de atributos próprios só aparecem no newInstance().