O PHP evoluiu muito a partir das versões 8.0 e 8.1, trazendo dois recursos que mudaram a forma de estruturar sistemas modernos: Atributos e Enums.
- Enums trazem tipos seguros, eliminando valores soltos ou “strings mágicas”.
- Atributos permitem adicionar metadados ao código, de forma clara e declarativa.
Neste artigo, vamos ver como esses dois recursos funcionam em conjunto, criando um sistema simples e elegante para validação.
A ideia é demonstrar, na prática, como Atributos e Enums no PHP tornam o código:
- 🔐 mais seguro
- 😎 mais legível
- 👍 mais expressivo
- 💪 mais fácil de evoluir
Pré-requisitos
Nós vamos usar Atributos e Enums. Ou seja, é essencial que você conheça os fundamentos desses dois recursos do PHP.
E veja que boa notícia: eu escrevi dois artigos bem completos sobre eles! 😎
Clique aqui para ler o meu artigo sobre Enums no PHP
Clique aqui para ler o meu artigo sobre Atributos no PHP
Visão geral da aplicação
A combinação de Enums e Atributos é muito comum para criação de sistemas de validação, definindo atributos obrigatórios, seus tipos, regras e padrões.
Aqui vamos fazer algo semelhante, porém diferente.
Vamos criar um sistema de validação de pratos de refeição! 😋
Teremos classes representando os ingredientes e, ao montar um prato com eles, o sistema deve identificar se incluímos todos os macro-nutrientes: proteína, carboidrato e gordura.
Vou mostrar a criação de cada arquivo. Mas você também pode ver a aplicação completa neste repositório do Github.
Estrutura do projeto
A aplicação consiste em:
- Enums para definir as categorias nutricionais
- Atributos para definir a qual categoria (macro-nutriente) cada alimento pertence
- Classes de alimentos, anotadas com Atributos
- Um modelo Dish representando um prato
- Um validador, baseado em
Reflection, que analisa os metadados e valida o prato
Essa abordagem representa um caso real de uso muito comum em sistemas modernos:
usar atributos para declarar regras, e enums para garantir valores válidos
Criando o Enum de categorias alimentares
Crie o arquivo enums/Category.php com este conteúdo:
<?php
namespace Enums;
enum Category: string
{
case PROTEIN = 'protein';
case CARB = 'carb';
case FAT = 'fat';
}
Esse Enum garante que os tipos de alimento sejam sempre válidos, sem strings soltas pelo código.
Criando o Atributo que define a categoria de cada alimento
Crie o arquivo Attributes/FoodCategory.php com o seguinte conteúdo:
<?php
namespace Attributes;
use Attribute;
use Enums\Category;
#[Attribute(Attribute::TARGET_CLASS)]
class FoodCategory
{
public function __construct(public Category $category)
{}
}
O Atributo FoodCategory declara metadados associados a cada alimento.
Por que usamos TARGET_CLASS?
Porque esse atributo deve ser aplicado apenas em classes de alimentos, e não em métodos ou propriedades.
O TARGET garante que o uso incorreto causará erro imediato, prevenindo bugs e aumentando a confiabilidade.
Criando os alimentos com Atributos
Cada alimento será uma classe com atributos que lhe caracterizam como categorias alimentares.
Vamos criar o namespace Foods, e os seguintes arquivos:
Foods/Almonds.php
Foods/Avocado.php
Foods/BlackBeans.php
Foods/Chickpeas.php
Foods/Lentils.php
Foods/Quinoa.php
Foods/Rice.php
Foods/SweetPotato.php
Foods/Tahini.php
Foods/Tofu.php
Os conteúdos dos arquivos serão os seguintes:
// Foods/Almonds.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::FAT)]
class Almonds
{}
//----------------
// Foods/Avocado.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::FAT)]
class Avocado
{}
//----------------
// Foods/BlackBeans.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::PROTEIN)]
class BlackBeans
{}
//----------------
// Foods/Chickpeas.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::PROTEIN)]
class Chickpeas
{}
//----------------
// Foods/Lentils.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::PROTEIN)]
class Lentils
{}
//----------------
// Foods/Quinoa.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::CARB)]
class Quinoa
{}
//----------------
// Foods/Rice.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::CARB)]
class Rice
{}
//----------------
// Foods/SweetPotato.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::CARB)]
class SweetPotato
{}
//----------------
// Foods/Tahini.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::FAT)]
class Tahini
{}
//----------------
// Foods/Tofu.php
<?php
namespace Foods;
use Attributes\FoodCategory;
use Enums\Category;
#[FoodCategory(Category::PROTEIN)]
class Tofu
{}
Essa estrutura permite a fácil criação de novos alimentos. Para isso, basta criar uma nova classe, com o atributo correspondente à categoria do alimento.
Ou seja, é uma estrutura clara e expansível.
Criando o modelo que representará um prato
Um prato pode ser tratado como um modelo (Model).
Ele é, em resumo, uma lista de alimentos. Portanto vamos criar uma classe cujo construtor recebe um array.
Crie o arquivo Models/Dish.php com este conteúdo:
<?php
namespace Models;
class Dish
{
public function __construct(public array $items)
{}
}
Criando o serviço de validação
Aqui acontece a parte mais interessante, onde iremos ler os atributos e fazer as devidas validações.
O validador irá:
- Analisar os alimentos via Reflection
- Ler o Attribute
FoodCategoryde cada classe - Identificar as categorias presentes
- Comparar com o Enum
Category - Validar se o prato está completo
Crie o arquivo Services/DishValidator.php assim:
<?php
namespace Services;
use ReflectionClass;
use Attributes\FoodCategory;
use Enums\Category;
use Exception;
use Models\Dish;
class DishValidator
{
public static function validate(Dish $dish): void
{
$categoriesFound = [];
foreach ($dish->items as $item) {
$reflection = new ReflectionClass($item);
$attribute = $reflection->getAttributes(FoodCategory::class)[0] ?? null;
// verifica se existe algum atributo na classe
if (!$attribute) {
throw new Exception("The " . $reflection->getShortName() . " item does not have a defined category.");
}
/** @var FoodCategory $instance */
$instance = $attribute->newInstance();
// adiciona a categoria à lista de categorias encontradas
$categoriesFound[] = $instance->category;
}
// verifica se todas as categorias estão presentes
foreach (Category::cases() as $required) {
if (!in_array($required, $categoriesFound, true)) {
throw new Exception("The dish is incomplete: missing an item of type {$required->value}.");
}
}
}
}
E basta isso para a validação!
O validador não precisa saber quais alimentos existem.
Ele apenas lê os Atributos e os Enums.
Adicionar novos alimentos não quebra nada.
A estrutura é completamente extensível! 😎
Testando o validador
Para testarmos o validador, primeiro vamos criar um arquivo de autoload, para que as classes sejam carregadas corretamente conforme seus namespaces.
Crie o arquivo autoload.php:
<?php
/**
* Minimal PSR-4 style autoloader.
*/
spl_autoload_register(function (string $class): void {
$baseDir = __DIR__ . DIRECTORY_SEPARATOR;
$relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
$file = $baseDir . $relativePath;
if (is_file($file)) {
require_once $file;
}
});
E agora vamos criar o index.php com alguns pratos completos e incompletos, para vermos as validações em ação:
<?php
require_once __DIR__ . '/autoload.php';
use Models\Dish;
use Services\DishValidator;
// função que chama o validador e gera saídas de texto personalizadas
function validateDish(string $name, Dish $dish): void {
try {
DishValidator::validate($dish);
echo "✅ {$name}: has protein, carb, and fat." . PHP_EOL;
} catch (Exception $e) {
echo "❌ {$name}: " . $e->getMessage() . PHP_EOL;
}
}
// lista de pratos, com versões válidas e inválidas
$dishes = [
'Classic Lentil Bowl' => new Dish([
new Foods\Lentils(), // Protein
new Foods\Rice(), // Carb
new Foods\Avocado(), // Fat
]),
'Mediterranean Power Plate' => new Dish([
new Foods\Chickpeas(), // Protein
new Foods\Quinoa(), // Carb
new Foods\Tahini(), // Fat
]),
'Sweet Harvest Bowl' => new Dish([
new Foods\BlackBeans(), // Protein
new Foods\SweetPotato(), // Carb
new Foods\Almonds(), // Fat
]),
'Power Protein dish' => new Dish([
new Foods\BlackBeans(), // Protein
new Foods\Lentils(), // Protein
new Foods\Tofu(), // Protein
new Foods\SweetPotato(), // Carb
new Foods\Almonds(), // Fat
]),
'Missing Carb Combo' => new Dish([
new Foods\Lentils(), // Protein
new Foods\Avocado(), // Fat
new Foods\Almonds(), // Fat
]),
'Missing Fat Combo' => new Dish([
new Foods\Chickpeas(), // Protein
new Foods\Quinoa(), // Carb
new Foods\Rice(), // Carb
]),
'Protein-Only Feast' => new Dish([
new Foods\Chickpeas(), // Protein
new Foods\BlackBeans(), // Protein
new Foods\Lentils(), // Protein
]),
];
foreach ($dishes as $name => $dish) {
validateDish($name, $dish);
}
Podemos executar o script no próprio terminal:
php index.php
Teremos a seguinte saída:
✅ Classic Lentil Bowl: has protein, carb, and fat.
✅ Mediterranean Power Plate: has protein, carb, and fat.
✅ Sweet Harvest Bowl: has protein, carb, and fat.
✅ Power Protein dish: has protein, carb, and fat.
❌ Missing Carb Combo: The dish is incomplete: missing an item of type carb.
❌ Missing Fat Combo: The dish is incomplete: missing an item of type fat.
❌ Protein-Only Feast: The dish is incomplete: missing an item of type carb.
Você pode brincar com o cardápio, criar novos pratos, novos alimentos.
Desde que você siga o mesmo padrão, as validações vão sempre funcionar.
O poder da combinação de Atributos e Enums
Afinal, por que Atributos e Enums funcionam tão bem juntos?
Porque atributos declaram a intenção. E Enums garantem a validade dos valores.
Eles criam juntos:
- código mais limpo
- validações automáticas
- modelos mais seguros
- um domínio mais explícito
- uma arquitetura extensível
Combinar Enums e Atributos abre portas para sistemas muito mais expressivos e seguros.
O exemplo de validação de pratos demonstra como esses recursos tornam o código autoexplicativo, fácil de evoluir e alinhado com as melhores práticas modernas.
Esse tipo de arquitetura pode ser aplicada em:
- validação de domínio
- regras de negócio declarativas
- sistemas de rotas
- serialização
- validação de formulários
- processamento dinâmico
- API-first design
Aproveite esse padrão no seu projeto e você verá o quanto o desenvolvimento se torna mais limpo e previsível.
Disclaimer: Termo de Responsabilidade
Nenhum animal foi explorado ou morto para a montagem desses pratos! 💚🌱
Faça a sua parte e mantenha nosso cardápio limpo e livre de maus tratos. #GoVegan
Código-Fonte Completo no Github
O código-fonte completo da aplicação que montamos aqui está neste repositório do Github.

Sou Roberto Beraldo (ou apenas Beraldo), desenvolvedor PHP há mais de 15 anos, numa jornada de desenvolvimento para me aprofundar no mundo DevOps e Cloud. Bacharel em Ciência da Computação, com uma base sólida em desenvolvimento web e conhecimentos em DevOps e Computação em Nuvem. Estou dedicado a conectar o desenvolvimento de software com a gestão de infraestrutura.