PHP 8.x Moderno

PHP 8.x Moderno

Enums, readonly, atributos e fibers mudaram o que se considera código PHP idiomático. O que cada recurso resolve, em que versão entrou e quando vale adotar — incluindo os casos em que o recurso novo não substitui o jeito antigo, apenas oferece uma alternativa com outro custo.
PHP

23 min de leitura

O PHP 8.x trouxe as mudanças mais significativas da linguagem em uma década. Enums nativos eliminam a necessidade de constantes mágicas e classes de constantes. Fibers introduzem concorrência cooperativa no núcleo da linguagem. Readonly classes tornam objetos de valor imutáveis com uma única palavra-chave. Atributos nativos substituem comentários PHPDoc para metadados estruturados e verificáveis.

Esses recursos não são apenas conveniências sintáticas — eles mudam a forma como você modela domínios, escreve código concorrente e documenta comportamentos esperados. Frameworks modernos já os adotam intensamente: o Laravel usa Enums em cast de Eloquent, usa Atributos para rotas e injeção de dependência, e o PHP-DI usa Atributos para configurar containers.

Enums nativos — PHP 8.1

Antes do PHP 8.1, simular enumerações exigia classes com constantes, bibliotecas externas como myclabs/php-enum, ou simplesmente strings e inteiros soltos. Enums nativos resolvem isso com suporte do tipo system: um enum é um tipo, não apenas um valor.

PHP tem dois tipos de enum: Pure Enums (sem valor associado) e Backed Enums (associados a string ou int).

<?php
declare(strict_types=1);

// ── PURE ENUM — sem valor subjacente ─────────────────────────────────
enum Naipe
{
    case Copas;
    case Ouros;
    case Espadas;
    case Paus;
}

$carta = Naipe::Copas;
echo $carta->name . "\n"; // Copas

// Enums são tipos — type hints funcionam
function jogar(Naipe $naipe): string
{
    return "Jogou: " . $naipe->name;
}
echo jogar(Naipe::Espadas) . "\n"; // Jogou: Espadas
// jogar("Copas") → TypeError — string não é Naipe

// ── BACKED ENUM — com valor string ───────────────────────────────────
enum Status: string
{
    case Pendente   = 'pendente';
    case Aprovado   = 'aprovado';
    case Cancelado  = 'cancelado';
    case Entregue   = 'entregue';

    // Métodos em enums — lógica de domínio junto ao tipo
    public function label(): string
    {
        return match($this) {
            Status::Pendente  => '⏳ Pendente',
            Status::Aprovado  => '✅ Aprovado',
            Status::Cancelado => '❌ Cancelado',
            Status::Entregue  => '📦 Entregue',
        };
    }

    public function podeSerCancelado(): bool
    {
        return in_array($this, [Status::Pendente, Status::Aprovado]);
    }
}

// Backed Enums: from() lança ValueError, tryFrom() retorna null
$status = Status::from('aprovado');          // Status::Aprovado
$status = Status::tryFrom('invalido');       // null — não lança exceção
echo Status::Pendente->value . "\n";         // pendente
echo Status::Aprovado->label() . "\n";       // ✅ Aprovado

// Listar todos os casos
foreach (Status::cases() as $caso) {
    echo $caso->value . " → " . $caso->label() . "\n";
}

// ── ENUM IMPLEMENTANDO INTERFACE ──────────────────────────────────────
interface TemLabel
{
    public function label(): string;
}

enum Prioridade: int implements TemLabel
{
    case Baixa  = 1;
    case Media  = 2;
    case Alta   = 3;
    case Critica = 4;

    public function label(): string
    {
        return match($this) {
            Prioridade::Baixa   => '🟢 Baixa',
            Prioridade::Media   => '🟡 Média',
            Prioridade::Alta    => '🟠 Alta',
            Prioridade::Critica => '🔴 Crítica',
        };
    }

    // Constantes em enums
    const PADRAO = self::Media;
}

echo Prioridade::Alta->label() . "\n";      // 🟠 Alta
echo Prioridade::PADRAO->label() . "\n";    // 🟡 Média

Readonly classes — PHP 8.2

PHP 8.1 introduziu readonly em propriedades individuais. PHP 8.2 tornou possível marcar a classe inteira como readonly — todas as propriedades tipadas se tornam readonly automaticamente. É o modelo perfeito para Value Objects e DTOs.

<?php
declare(strict_types=1);

// PHP 8.1 — readonly em propriedades individuais
class PontoV1
{
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {}
}

// PHP 8.2 — readonly class — todas as propriedades são readonly automaticamente
readonly class Ponto
{
    public function __construct(
        public float $x,
        public float $y,
    ) {}

    // Readonly classes não podem ter propriedades não-readonly
    // mas podem ter métodos normais
    public function distanciaAte(Ponto $outro): float
    {
        return sqrt(($this->x - $outro->x) ** 2 + ($this->y - $outro->y) ** 2);
    }

    // Padrão wither — retorna nova instância com valor alterado (imutabilidade)
    public function comX(float $x): static
    {
        return new static($x, $this->y);
    }

    public function comY(float $y): static
    {
        return new static($this->x, $y);
    }
}

$a = new Ponto(0.0, 0.0);
$b = new Ponto(3.0, 4.0);

echo $a->distanciaAte($b) . "\n"; // 5
// $a->x = 1.0; → Error: Cannot modify readonly property

$c = $a->comX(1.0)->comY(2.0);   // nova instância — $a intacto
echo $c->x . ", " . $c->y . "\n"; // 1, 2

// Value Objects como Readonly Classes
readonly class Dinheiro
{
    public function __construct(
        public int    $centavos,
        public string $moeda,
    ) {}

    public function somar(Dinheiro $outro): static
    {
        if ($this->moeda !== $outro->moeda) {
            throw new \InvalidArgumentException("Moedas diferentes: {$this->moeda} e {$outro->moeda}");
        }
        return new static($this->centavos + $outro->centavos, $this->moeda);
    }

    public function formatado(): string
    {
        return number_format($this->centavos / 100, 2, ',', '.') . " {$this->moeda}";
    }
}

$preco    = new Dinheiro(1999, 'BRL');
$frete    = new Dinheiro(1500, 'BRL');
$total    = $preco->somar($frete);
echo $total->formatado() . "\n"; // 34,99 BRL

Atributos nativos — PHP 8.0+

Atributos (antes chamados de Annotations em outras linguagens) permitem anexar metadados estruturados a classes, métodos, propriedades e parâmetros. Ao contrário dos comentários PHPDoc, atributos são verificados pelo PHP, podem receber parâmetros tipados, e são lidos via Reflection API com type safety.

<?php
declare(strict_types=1);

// Definindo um atributo — é uma classe com #[Attribute]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Rota
{
    public function __construct(
        public readonly string $caminho,
        public readonly string $metodo = 'GET',
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Validar
{
    public function __construct(
        public readonly string $tipo,        // 'required', 'email', 'min', etc.
        public readonly mixed  $parametro = null,
    ) {}
}

#[Attribute(Attribute::TARGET_METHOD)]
class Cache
{
    public function __construct(
        public readonly int    $ttlSegundos = 300,
        public readonly string $prefixo     = '',
    ) {}
}

// Usando os atributos
#[Rota('/api/usuarios')]
class UsuarioController
{
    #[Rota('/api/usuarios/{id}', 'GET')]
    #[Cache(ttlSegundos: 60, prefixo: 'usuario')]
    public function mostrar(int $id): array
    {
        return ['id' => $id];
    }

    #[Rota('/api/usuarios', 'POST')]
    public function criar(
        #[Validar('required')]
        #[Validar('email')]
        string $email,

        #[Validar('required')]
        #[Validar('min', 8)]
        string $senha,
    ): array {
        return ['criado' => true];
    }
}

// Lendo atributos via Reflection — como frameworks fazem
$reflection = new ReflectionClass(UsuarioController::class);

// Lê atributo da classe
$atributosClasse = $reflection->getAttributes(Rota::class);
foreach ($atributosClasse as $attr) {
    $rota = $attr->newInstance();
    echo "Classe: {$rota->caminho} [{$rota->metodo}]\n";
    // Classe: /api/usuarios [GET]
}

// Lê atributos dos métodos
foreach ($reflection->getMethods() as $metodo) {
    foreach ($metodo->getAttributes(Rota::class) as $attr) {
        $rota = $attr->newInstance();
        echo "Método {$metodo->getName()}: {$rota->caminho} [{$rota->metodo}]\n";
    }
    foreach ($metodo->getAttributes(Cache::class) as $attr) {
        $cache = $attr->newInstance();
        echo "Cache: TTL={$cache->ttlSegundos}s prefixo='{$cache->prefixo}'\n";
    }
}

Fibers — PHP 8.1

Fibers são corrotinas de baixo nível — blocos de código que podem ser pausados e retomados, cedendo o controle ao chamador. Não são threads (não rodam em paralelo), mas permitem programação assíncrona cooperativa: múltiplas tarefas progridem intercaladas sem bloquear o processo.

Fibers são a base sobre a qual bibliotecas como ReactPHP e Amp implementam seus loops de eventos no PHP moderno.

<?php
declare(strict_types=1);

// Fiber básico — suspender e retomar
$fiber = new Fiber(function (): void {
    echo "Fiber: início\n";

    // Suspende e passa um valor para quem chamou resume()
    $valor = Fiber::suspend('primeiro suspend');
    echo "Fiber: retomado com '{$valor}'\n";

    $valor2 = Fiber::suspend('segundo suspend');
    echo "Fiber: retomado novamente com '{$valor2}'\n";

    echo "Fiber: fim\n";
});

// start() executa até o primeiro Fiber::suspend()
$resultado = $fiber->start();
echo "Código principal recebeu: '{$resultado}'\n";

// resume() continua de onde parou, passando um valor para o Fiber
$resultado2 = $fiber->resume('ping');
echo "Código principal recebeu: '{$resultado2}'\n";

$fiber->resume('pong');

echo "Fiber terminado: " . ($fiber->isTerminated() ? "sim" : "não") . "\n";

// Output:
// Fiber: início
// Código principal recebeu: 'primeiro suspend'
// Fiber: retomado com 'ping'
// Código principal recebeu: 'segundo suspend'
// Fiber: retomado novamente com 'pong'
// Fiber: fim
// Fiber terminado: sim

// ── SCHEDULER SIMPLES — demonstrando concorrência cooperativa ──────────
class Scheduler
{
    /** @var Fiber[] */
    private array $fibers = [];

    public function adicionar(Fiber $fiber): void
    {
        $this->fibers[] = $fiber;
    }

    public function executar(): void
    {
        // Inicia todos os fibers
        foreach ($this->fibers as $fiber) {
            $fiber->start();
        }

        // Continua rodando até todos terminarem
        while (!empty($this->fibers)) {
            foreach ($this->fibers as $i => $fiber) {
                if ($fiber->isSuspended()) {
                    $fiber->resume();
                }
                if ($fiber->isTerminated()) {
                    unset($this->fibers[$i]);
                }
            }
        }
    }
}

$scheduler = new Scheduler();

$scheduler->adicionar(new Fiber(function (): void {
    for ($i = 1; $i <= 3; $i++) {
        echo "Tarefa A — passo {$i}\n";
        Fiber::suspend(); // cede o controle
    }
}));

$scheduler->adicionar(new Fiber(function (): void {
    for ($i = 1; $i <= 3; $i++) {
        echo "Tarefa B — passo {$i}\n";
        Fiber::suspend(); // cede o controle
    }
}));

$scheduler->executar();
// Tarefa A — passo 1
// Tarefa B — passo 1
// Tarefa A — passo 2
// Tarefa B — passo 2
// Tarefa A — passo 3
// Tarefa B — passo 3

Outros recursos PHP 8.x notáveis

<?php
declare(strict_types=1);

// ── NAMED ARGUMENTS — PHP 8.0 ─────────────────────────────────────────
// Argumentos nomeados — ordem não importa, leitura mais clara
$resultado = array_slice(array: [1,2,3,4,5], offset: 1, length: 3);
// [2, 3, 4]

// Especialmente útil com construtores complexos
$data = new \DateTimeImmutable(
    datetime: '2024-01-15',
    timezone: new \DateTimeZone('America/Sao_Paulo'),
);

// ── MATCH EXPRESSION — PHP 8.0 ────────────────────────────────────────
// match é uma expressão (tem retorno), comparação estrita, sem fallthrough
$codigo = 200;
$descricao = match($codigo) {
    200, 201  => 'Sucesso',
    400       => 'Requisição inválida',
    401, 403  => 'Não autorizado',
    404       => 'Não encontrado',
    500       => 'Erro interno',
    default   => 'Código desconhecido',
};

// ── NULLSAFE OPERATOR — PHP 8.0 ───────────────────────────────────────
// Encadeia chamadas sem verificar null em cada passo
$cidade = $usuario?->endereco?->cidade?->nome;
// Equivale a: $usuario && $usuario->endereco && ... — mas conciso

// ── FIRST CLASS CALLABLES — PHP 8.1 ──────────────────────────────────
// Cria Closure a partir de qualquer callable com sintaxe limpa
$fn = strlen(...);         // Closure de função nativa
echo $fn("olá") . "\n";  // 3

$dobrar = fn(int $n): int => $n * 2;
$numeros = [1, 2, 3, 4, 5];
$dobrados = array_map($dobrar, $numeros); // [2, 4, 6, 8, 10]

// ── INTERSECTION TYPES — PHP 8.1 ──────────────────────────────────────
// Já vimos no artigo Traits Avançados — objetos que satisfazem múltiplas interfaces

// ── NEVER RETURN TYPE — PHP 8.1 ───────────────────────────────────────
// Declara que a função NUNCA retorna — sempre lança exceção ou termina o processo
function falharSempre(string $mensagem): never
{
    throw new \RuntimeException($mensagem);
}

// ── DNF TYPES — PHP 8.2 ───────────────────────────────────────────────
// Disjunctive Normal Form — combinação de union e intersection types
interface Stringable { public function __toString(): string; }
interface Countable  { public function count(): int; }
interface JsonSerializable { public function jsonSerialize(): mixed; }

// Aceita (Stringable & Countable) OU JsonSerializable
function processar((Stringable&Countable)|JsonSerializable $dado): void
{
    // ...
}

O PHP 8 encurtou a distância entre o que se quer dizer e o que se precisa escrever: um estado fixo virou enum em vez de constante solta, imutabilidade virou palavra-chave em vez de convenção, metadado virou atributo em vez de comentário interpretado. Nenhum desses recursos é obrigatório, e a base instalada vai continuar cheia de código que não os usa por muitos anos. Mas quando aparecer a escolha em código novo, vale lembrar que a versão mínima do projeto é uma decisão — e adiar a subida custa mais do que adotá-los.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Modele um sistema de pedidos usando Enums: StatusPedido (backed string com métodos label(), podeTransicionarPara(StatusPedido $novo): bool), MetodoPagamento (backed string: Pix, Cartao, Boleto) e TipoPrioridade (backed int: 1-3). Garanta que transições inválidas sejam bloqueadas.

Ver resposta

✓ Resposta: Colocar proximosPossiveis() dentro do próprio enum é o que impede a regra de espalhar: a máquina de estados mora junto do estado. E prefira tryFrom() a from() para dado que vem de fora — from() lança ValueError em valor desconhecido.

<?php

declare(strict_types=1);

enum StatusPedido: string
{
    case Rascunho  = 'rascunho';
    case Aguardando = 'aguardando_pagamento';
    case Pago      = 'pago';
    case Separando = 'separando';
    case Enviado   = 'enviado';
    case Entregue  = 'entregue';
    case Cancelado = 'cancelado';

    public function label(): string
    {
        return match ($this) {
            self::Rascunho   => 'Rascunho',
            self::Aguardando => 'Aguardando pagamento',
            self::Pago       => 'Pago',
            self::Separando  => 'Em separação',
            self::Enviado    => 'Enviado',
            self::Entregue   => 'Entregue',
            self::Cancelado  => 'Cancelado',
        };
    }

    /** Estados dos quais não se sai mais. */
    public function ehFinal(): bool
    {
        return in_array($this, [self::Entregue, self::Cancelado], true);
    }

    /** @return self[] */
    public function proximosPossiveis(): array
    {
        return match ($this) {
            self::Rascunho   => [self::Aguardando, self::Cancelado],
            self::Aguardando => [self::Pago, self::Cancelado],
            self::Pago       => [self::Separando, self::Cancelado],
            self::Separando  => [self::Enviado, self::Cancelado],
            self::Enviado    => [self::Entregue],
            self::Entregue,
            self::Cancelado  => [],
        };
    }

    public function podeTransicionarPara(StatusPedido $novo): bool
    {
        return in_array($novo, $this->proximosPossiveis(), true);
    }
}

enum MetodoPagamento: string
{
    case Pix    = 'pix';
    case Cartao = 'cartao';
    case Boleto = 'boleto';

    public function prazoCompensacaoEmDias(): int
    {
        return match ($this) {
            self::Pix    => 0,
            self::Cartao => 1,
            self::Boleto => 3,
        };
    }

    public function taxa(): float
    {
        return match ($this) {
            self::Pix    => 0.0099,
            self::Cartao => 0.0399,
            self::Boleto => 0.0249,
        };
    }
}

enum TipoPrioridade: int
{
    case Baixa  = 1;
    case Normal = 2;
    case Alta   = 3;

    public function prazoEmHoras(): int
    {
        return match ($this) {
            self::Baixa  => 72,
            self::Normal => 48,
            self::Alta   => 12,
        };
    }
}

final class TransicaoInvalidaException extends DomainException
{
    public function __construct(StatusPedido $de, StatusPedido $para)
    {
        parent::__construct(
            "Transição inválida: {$de->label()} → {$para->label()}."
        );
    }
}

final class Pedido
{
    private StatusPedido $status = StatusPedido::Rascunho;

    /** @var array<int, array{de: string, para: string, em: string}> */
    private array $historico = [];

    public function __construct(
        public readonly MetodoPagamento $pagamento,
        public readonly TipoPrioridade $prioridade = TipoPrioridade::Normal,
    ) {}

    public function mudarPara(StatusPedido $novo): void
    {
        if (!$this->status->podeTransicionarPara($novo)) {
            throw new TransicaoInvalidaException($this->status, $novo);
        }

        $this->historico[] = [
            'de'   => $this->status->value,
            'para' => $novo->value,
            'em'   => (new DateTimeImmutable())->format('c'),
        ];

        $this->status = $novo;
    }

    public function status(): StatusPedido { return $this->status; }
}

$pedido = new Pedido(MetodoPagamento::Pix, TipoPrioridade::Alta);

$pedido->mudarPara(StatusPedido::Aguardando);
$pedido->mudarPara(StatusPedido::Pago);

echo $pedido->status()->label(), PHP_EOL;   // Pago

try {
    $pedido->mudarPara(StatusPedido::Entregue);   // pulou etapas
} catch (TransicaoInvalidaException $e) {
    echo '✗ ', $e->getMessage(), PHP_EOL;
    // ✗ Transição inválida: Pago → Entregue.
}

// Backed enum converte nos dois sentidos:
$vindoDoBanco = StatusPedido::from('pago');
$talvez = StatusPedido::tryFrom('inexistente');   // null, não explode

Exercício 2

Crie uma readonly class Endereco com campos logradouro, numero, complemento (nullable), cidade, estado, cep. Implemente métodos formatado(): string e comComplemento(string $c): static. Crie também readonly class Cliente que compõe um Endereco.

Ver resposta

✓ Resposta: Dois limites que pegam quem está começando com readonly: a propriedade só pode ser escrita de dentro do escopo da classe, e o clone continua raso — clonar um objeto que contém outro objeto mutável compartilha o interno. Com composição de readonly, como aqui, isso deixa de ser problema.

<?php

declare(strict_types=1);

// PHP 8.2: readonly na classe marca TODAS as propriedades como readonly.
// Não é preciso repetir a palavra em cada uma.
readonly class Endereco
{
    public function __construct(
        public string $logradouro,
        public string $numero,
        public ?string $complemento,
        public string $cidade,
        public string $estado,
        public string $cep,
    ) {
        // A validação vive no construtor: um Endereco que existe é válido.
        if (!preg_match('/^\d{5}-?\d{3}$/', $cep)) {
            throw new InvalidArgumentException("CEP inválido: {$cep}");
        }

        if (mb_strlen($estado) !== 2) {
            throw new InvalidArgumentException("UF inválida: {$estado}");
        }
    }

    public function formatado(): string
    {
        $linha = "{$this->logradouro}, {$this->numero}";

        if ($this->complemento !== null && trim($this->complemento) !== '') {
            $linha .= " — {$this->complemento}";
        }

        return sprintf('%s, %s/%s, CEP %s',
                       $linha, $this->cidade, mb_strtoupper($this->estado),
                       $this->cepFormatado());
    }

    public function cepFormatado(): string
    {
        $digitos = preg_replace('/\D/', '', $this->cep);

        return substr($digitos, 0, 5) . '-' . substr($digitos, 5);
    }

    // "Modificar" um objeto imutável = devolver uma cópia alterada.
    // O retorno `static` mantém o tipo em subclasses.
    public function comComplemento(string $c): static
    {
        return new static(
            $this->logradouro, $this->numero, $c,
            $this->cidade, $this->estado, $this->cep,
        );
    }

    public function igualA(self $outro): bool
    {
        // Objeto de valor: igualdade é por conteúdo, não por identidade.
        return $this->cepFormatado() === $outro->cepFormatado()
            && $this->numero === $outro->numero;
    }
}

readonly class Cliente
{
    public function __construct(
        public string $nome,
        public string $email,
        public Endereco $endereco,
        public ?DateTimeImmutable $nascimento = null,
    ) {}

    public function mudarEndereco(Endereco $novo): static
    {
        return new static($this->nome, $this->email, $novo, $this->nascimento);
    }

    public function etiqueta(): string
    {
        return $this->nome . PHP_EOL . $this->endereco->formatado();
    }
}

$endereco = new Endereco(
    logradouro: 'Av. Paulista',
    numero: '1578',
    complemento: null,
    cidade: 'São Paulo',
    estado: 'SP',
    cep: '01310100',
);

echo $endereco->formatado(), PHP_EOL;
// Av. Paulista, 1578, São Paulo/SP, CEP 01310-100

$comSala = $endereco->comComplemento('Sala 42');
echo $comSala->formatado(), PHP_EOL;
// Av. Paulista, 1578 — Sala 42, São Paulo/SP, CEP 01310-100

// O original continua intacto — é o ponto da imutabilidade.
var_dump($endereco->complemento);   // NULL

$cliente = new Cliente('Ana Souza', 'ana@exemplo.com', $endereco);
echo $cliente->etiqueta(), PHP_EOL;

// $endereco->numero = '1600';
// Error: Cannot modify readonly property Endereco::$numero

Exercício 3

Defina três atributos: #[ValidarEmail], #[ValidarMinimo(int $min)] e #[ValidarMaximo(int $max)]. Crie uma função validarObjeto(object $obj): array que usa Reflection para ler os atributos das propriedades e retorna os erros encontrados.

Ver resposta

✓ Resposta: Atributo sozinho é metadado inerte: ele não faz nada até alguém lê-lo por Reflection. E note o custo — Reflection não é grátis. Em caminho quente, o padrão é ler uma vez e guardar o resultado em cache, que é exatamente o que os frameworks fazem ao compilar rotas e validações.

<?php

declare(strict_types=1);

#[Attribute(Attribute::TARGET_PROPERTY)]
final class ValidarEmail
{
    public function __construct(public readonly string $mensagem = 'E-mail inválido.') {}
}

// REPEATABLE permite pôr o mesmo atributo mais de uma vez na propriedade.
#[Attribute(Attribute::TARGET_PROPERTY)]
final class ValidarMinimo
{
    public function __construct(
        public readonly int $min,
        public readonly ?string $mensagem = null,
    ) {}

    public function mensagem(string $campo): string
    {
        return $this->mensagem
            ?? "O campo {$campo} precisa de ao menos {$this->min} caractere(s).";
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
final class ValidarMaximo
{
    public function __construct(
        public readonly int $max,
        public readonly ?string $mensagem = null,
    ) {}

    public function mensagem(string $campo): string
    {
        return $this->mensagem
            ?? "O campo {$campo} aceita no máximo {$this->max} caractere(s).";
    }
}

/** @return array<string, string[]> campo => erros */
function validarObjeto(object $obj): array
{
    $erros = [];
    $reflexao = new ReflectionObject($obj);

    foreach ($reflexao->getProperties() as $propriedade) {
        $atributos = $propriedade->getAttributes();

        if ($atributos === []) {
            continue;
        }

        // Propriedade private também é lida — mas só depois disto.
        $propriedade->setAccessible(true);
        $valor = $propriedade->isInitialized($obj) ? $propriedade->getValue($obj) : null;
        $campo = $propriedade->getName();

        foreach ($atributos as $atributo) {
            // newInstance() é o que transforma a anotação em objeto de verdade,
            // com os argumentos que foram escritos entre colchetes.
            $regra = $atributo->newInstance();

            $erro = match (true) {
                $regra instanceof ValidarEmail =>
                    (!is_string($valor) || !filter_var($valor, FILTER_VALIDATE_EMAIL))
                        ? $regra->mensagem : null,

                $regra instanceof ValidarMinimo =>
                    (is_string($valor) && mb_strlen($valor) < $regra->min)
                    || (is_numeric($valor) && $valor < $regra->min)
                        ? $regra->mensagem($campo) : null,

                $regra instanceof ValidarMaximo =>
                    (is_string($valor) && mb_strlen($valor) > $regra->max)
                    || (is_numeric($valor) && $valor > $regra->max)
                        ? $regra->mensagem($campo) : null,

                default => null,
            };

            if ($erro !== null) {
                $erros[$campo][] = $erro;
            }
        }
    }

    return $erros;
}

final class CadastroUsuario
{
    public function __construct(
        #[ValidarMinimo(3)]
        #[ValidarMaximo(60)]
        public string $nome = '',

        #[ValidarEmail('Informe um e-mail de verdade.')]
        public string $email = '',

        #[ValidarMinimo(8, 'A senha precisa de 8 caracteres ou mais.')]
        private string $senha = '',

        #[ValidarMinimo(18)]
        #[ValidarMaximo(120)]
        public int $idade = 0,
    ) {}
}

print_r(validarObjeto(new CadastroUsuario(
    nome: 'Jo',
    email: 'nao-eh-email',
    senha: '123',
    idade: 15,
)));

// [nome]  => O campo nome precisa de ao menos 3 caractere(s).
// [email] => Informe um e-mail de verdade.
// [senha] => A senha precisa de 8 caracteres ou mais.
// [idade] => O campo idade precisa de ao menos 18 caractere(s).

print_r(validarObjeto(new CadastroUsuario(
    'Ana Souza', 'ana@exemplo.com', 'senha-bem-longa', 34,
)));
// Array ( )  — sem erros

Exercício 4

Escreva um mini-scheduler de Fibers que executa três "tarefas" em paralelo cooperativo: download simulado (5 passos), processamento (3 passos) e log (4 passos). Cada passo imprime o nome da tarefa e suspende. O scheduler intercala a execução.

Ver resposta

✓ Resposta: "Paralelo cooperativo" é o nome exato: nada roda ao mesmo tempo. Cada fiber corre até ela mesma chamar suspend() — uma tarefa que nunca suspende trava o scheduler inteiro. Fibers resolvem I/O concorrente, não CPU: para processamento pesado de verdade, o caminho é processo separado.

<?php

declare(strict_types=1);

final class Scheduler
{
    /** @var array<string, Fiber> */
    private array $tarefas = [];

    public function adicionar(string $nome, callable $tarefa): void
    {
        $this->tarefas[$nome] = new Fiber($tarefa);
    }

    public function executar(): void
    {
        // Round-robin: dá uma fatia a cada fiber, na ordem, até todas terminarem.
        while ($this->tarefas !== []) {
            foreach ($this->tarefas as $nome => $fiber) {
                if (!$fiber->isStarted()) {
                    $fiber->start($nome);
                } elseif ($fiber->isSuspended()) {
                    // resume() devolve o controle de onde o suspend() parou —
                    // com todo o estado local da função preservado.
                    $fiber->resume();
                }

                if ($fiber->isTerminated()) {
                    $retorno = $fiber->getReturn();

                    printf("✔ %s terminou%s%s", $nome,
                           $retorno !== null ? " ({$retorno})" : '', PHP_EOL);

                    unset($this->tarefas[$nome]);
                }
            }
        }
    }
}

$passo = static function (string $tarefa, int $n, int $total): void {
    printf("  %-14s passo %d/%d%s", $tarefa, $n, $total, PHP_EOL);

    // Devolve o controle ao scheduler. O ponto de parada fica guardado:
    // na volta, a execução continua na linha seguinte.
    Fiber::suspend();
};

$scheduler = new Scheduler();

$scheduler->adicionar('download', static function (string $nome) use ($passo): string {
    for ($i = 1; $i <= 5; $i++) {
        $passo($nome, $i, 5);
    }
    return '5 arquivos';
});

$scheduler->adicionar('processamento', static function (string $nome) use ($passo): string {
    for ($i = 1; $i <= 3; $i++) {
        $passo($nome, $i, 3);
    }
    return '3 lotes';
});

$scheduler->adicionar('log', static function (string $nome) use ($passo): string {
    for ($i = 1; $i <= 4; $i++) {
        $passo($nome, $i, 4);
    }
    return '4 entradas';
});

$scheduler->executar();

//   download       passo 1/5
//   processamento  passo 1/3
//   log            passo 1/4
//   download       passo 2/5
//   processamento  passo 2/3
//   log            passo 2/4
//   download       passo 3/5
//   processamento  passo 3/3
//   log            passo 3/4
//   download       passo 4/5
// ✔ processamento terminou (3 lotes)
//   log            passo 4/4
//   download       passo 5/5
// ✔ log terminou (4 entradas)
// ✔ download terminou (5 arquivos)

Exercício 5

Desafio: combine os recursos do artigo para criar um micro-framework de rotas: atributo #[Rota(string $path, string $metodo)], um Router que usa Reflection para descobrir controllers e métodos anotados, e mapeia requisições {metodo} {path} para o método correto. Use um Enum MetodoHttp para os verbos.

Ver resposta

✓ Resposta: Duas decisões carregam esse roteador: a ordenação que põe rota estática antes da parametrizada (senão /produtos/novo casaria com {id}), e a conversão de tipo lida do próprio parâmetro do método. Em produção, o passo seguinte é cachear o mapa — varrer atributos a cada requisição é caro.

<?php

declare(strict_types=1);

enum MetodoHttp: string
{
    case Get    = 'GET';
    case Post   = 'POST';
    case Put    = 'PUT';
    case Patch  = 'PATCH';
    case Delete = 'DELETE';

    public function temCorpo(): bool
    {
        return in_array($this, [self::Post, self::Put, self::Patch], true);
    }
}

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Rota
{
    public function __construct(
        public readonly string $path,
        public readonly MetodoHttp $metodo = MetodoHttp::Get,
        public readonly ?string $nome = null,
    ) {}
}

final class Router
{
    /** @var array<int, array{rota: Rota, classe: string, metodo: string, regex: string}> */
    private array $rotas = [];

    /** Varre os controllers e descobre as rotas pelos atributos. */
    public function registrarControllers(string ...$classes): void
    {
        foreach ($classes as $classe) {
            $reflexao = new ReflectionClass($classe);

            foreach ($reflexao->getMethods(ReflectionMethod::IS_PUBLIC) as $metodo) {
                foreach ($metodo->getAttributes(Rota::class) as $atributo) {
                    $rota = $atributo->newInstance();

                    $this->rotas[] = [
                        'rota'   => $rota,
                        'classe' => $classe,
                        'metodo' => $metodo->getName(),
                        'regex'  => $this->compilar($rota->path),
                    ];
                }
            }
        }

        // Rota estática vence rota com parâmetro: /produtos/novo antes de
        // /produtos/{id}. Sem isto, "novo" seria capturado como um id.
        usort($this->rotas, static fn(array $a, array $b): int =>
            substr_count($a['rota']->path, '{') <=> substr_count($b['rota']->path, '{'));
    }

    public function despachar(MetodoHttp $metodo, string $uri): mixed
    {
        $caminho = rtrim(parse_url($uri, PHP_URL_PATH) ?? '/', '/') ?: '/';

        foreach ($this->rotas as $registro) {
            if ($registro['rota']->metodo !== $metodo) {
                continue;
            }

            if (!preg_match($registro['regex'], $caminho, $m)) {
                continue;
            }

            $params = array_filter($m, 'is_string', ARRAY_FILTER_USE_KEY);

            $controller = new $registro['classe']();

            // Casa os parâmetros da URL com os nomes dos argumentos do método,
            // convertendo para o tipo declarado.
            return $controller->{$registro['metodo']}(
                ...$this->argumentos($registro['classe'], $registro['metodo'], $params)
            );
        }

        http_response_code(404);
        return ['erro' => 'rota_nao_encontrada', 'path' => $caminho];
    }

    private function compilar(string $path): string
    {
        $regex = preg_replace(
            '/\\\\\{(\w+)\\\\\}/',
            '(?<$1>[^/]+)',
            preg_quote(rtrim($path, '/') ?: '/', '#'),
        );

        return "#^{$regex}$#";
    }

    private function argumentos(string $classe, string $metodo, array $params): array
    {
        $args = [];

        foreach ((new ReflectionMethod($classe, $metodo))->getParameters() as $p) {
            $valor = $params[$p->getName()] ?? null;

            if ($valor === null) {
                $args[] = $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null;
                continue;
            }

            $tipo = $p->getType() instanceof ReflectionNamedType
                ? $p->getType()->getName() : 'string';

            $args[] = match ($tipo) {
                'int'   => (int) $valor,
                'float' => (float) $valor,
                'bool'  => filter_var($valor, FILTER_VALIDATE_BOOL),
                default => $valor,
            };
        }

        return $args;
    }
}

// ---------- Controllers: a rota mora junto do método ---------------------
final class ProdutoController
{
    #[Rota('/produtos', MetodoHttp::Get, nome: 'produtos.index')]
    public function index(): array
    {
        return ['produtos' => ['Teclado', 'Mouse']];
    }

    #[Rota('/produtos/novo', MetodoHttp::Get)]
    public function formulario(): array
    {
        return ['form' => 'novo produto'];
    }

    // O int em $id não é decorativo: o Router converte antes de chamar.
    #[Rota('/produtos/{id}', MetodoHttp::Get, nome: 'produtos.show')]
    public function mostrar(int $id): array
    {
        return ['id' => $id, 'tipo' => get_debug_type($id)];
    }

    #[Rota('/produtos', MetodoHttp::Post)]
    public function criar(): array
    {
        return ['criado' => true];
    }
}

$router = new Router();
$router->registrarControllers(ProdutoController::class);

print_r($router->despachar(MetodoHttp::Get, '/produtos'));
// [produtos] => Teclado, Mouse

print_r($router->despachar(MetodoHttp::Get, '/produtos/novo'));
// [form] => novo produto        ← a estática venceu a rota com {id}

print_r($router->despachar(MetodoHttp::Get, '/produtos/42'));
// [id] => 42  [tipo] => int     ← convertido pelo tipo do parâmetro

print_r($router->despachar(MetodoHttp::Post, '/produtos'));
// [criado] => 1
Comentários

Mais em PHP

Variáveis, Tipos de Dados e Operadores
Variáveis, Tipos de Dados e Operadores

Toda variável em PHP guarda um valor de algum tipo, e a linguagem converte…

Herança, Interfaces e Traits
Herança, Interfaces e Traits

Herdar, assinar um contrato ou compor: três mecanismos que resolvem problemas…

Design Patterns: Singleton, Factory e Builder
Design Patterns: Singleton, Factory e Builder

Três padrões de criação e o que cada um custa: o Singleton, que garante…