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 é #[...].
- 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 classeOverridedo PHP; semusenem barra, dentro de um namespace, o PHP procurariaSeuNamespace\Override. - ('/produtos', metodo: 'POST')
- Argumentos, como numa chamada de função, inclusive nomeados. Só valores constantes: textos, números, arrays, constantes,
newde 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.
#[?
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:
| Chamada | Aviso? |
|---|---|
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 acrescenteAttribute::IS_REPEATABLEpara 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étodo | Devolve |
|---|---|
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
| Necessidade | Use |
|---|---|
| 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 framework | Atributo próprio + Reflection |
| Documentação para pessoas | Comentários /** … */ |
| Comportamento do programa | Có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
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.
$ 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.
$ 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.
$ 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)].
$ 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 / item | Para quê | Desde |
|---|---|---|
#[Attribute] | Marca uma classe como atributo | 8.0 |
#[\SensitiveParameter] | Esconde o valor do parâmetro nos rastros | 8.2 |
#[\AllowDynamicProperties] | Permite propriedades dinâmicas na classe | 8.2 |
#[\Override] | Garante que o método substitui outro | 8.3 |
#[\Deprecated] | Marca função, método ou constante de classe como obsoleta | 8.4 |
#[\NoDiscard] e (void) | Avisa quando o retorno é ignorado | 8.5 |
ReflectionClass, ReflectionMethod, ReflectionProperty, ReflectionFunction | Examinam o código; todos têm getAttributes() | 5 / 8.0 |
ReflectionAttribute::getName(), getArguments(), newInstance() | Nome, argumentos, objeto do atributo | 8.0 |
Exercícios
1. Proteger a senha
Complete o atributo que esconde o valor do parâmetro $senha nos rastros de erro:
2. Ignorar de propósito
A função salvar() tem #[\NoDiscard]. Complete para chamá-la ignorando o resultado sem aviso:
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().