Atributos e Enums no PHP: dupla dinâmica para validações inteligentes

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á:

  1. Analisar os alimentos via Reflection
  2. Ler o Attribute FoodCategory de cada classe
  3. Identificar as categorias presentes
  4. Comparar com o Enum Category
  5. 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.

Related posts