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
- PHP Manual — Enumerations: https://www.php.net/manual/pt_BR/language.enumerations.php
- PHP Manual — Readonly Properties (8.1): https://www.php.net/manual/pt_BR/language.oop5.properties.php#language.oop5.properties.readonly-properties
- PHP Manual — Readonly Classes (8.2): https://www.php.net/manual/pt_BR/language.oop5.basic.php#language.oop5.basic.class.readonly
- PHP Manual — Attributes: https://www.php.net/manual/pt_BR/language.attributes.php
- PHP Manual — Fibers: https://www.php.net/manual/pt_BR/language.fibers.php
- PHP 8.0 Release Notes: https://www.php.net/releases/8.0/pt_BR.php
- PHP 8.1 Release Notes: https://www.php.net/releases/8.1/pt_BR.php
- PHP 8.2 Release Notes: https://www.php.net/releases/8.2/pt_BR.php
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