Interfaces são o principal mecanismo de abstração do PHP orientado a objetos. Você já sabe declarar uma interface e implementá-la — mas o uso profissional vai muito além disso. Neste artigo exploramos interfaces como contratos formais de comportamento, a diferença entre covariância e contravariância nos tipos de retorno e parâmetros, interfaces funcionais com Closures, segregação de interfaces (o "I" do SOLID), e como compor comportamentos complexos empilhando interfaces pequenas.
Esses conceitos são o que separa um sistema que "funciona" de um sistema que pode crescer sem dor: código que depende de interfaces, não de implementações, pode ter suas partes substituídas, testadas e estendidas de forma independente.
Interfaces como contratos formais
Uma interface é um contrato: quem implementa promete que o código cliente pode chamar esses métodos com esses tipos e receber esses retornos — independente de qual classe concreta esteja por baixo. O poder não está na interface em si, mas na inversão de dependência que ela permite: o código de alto nível depende da abstração, não da implementação.
<?php
declare(strict_types=1);
namespace MeuApp\Contratos;
// Interface como contrato — define o que, não o como
interface RepositorioInterface
{
public function buscarPorId(int $id): ?array;
public function buscarTodos(): array;
public function salvar(array $dados): int; // retorna o ID gerado
public function deletar(int $id): bool;
}
// Implementação para produção — usa MySQL
class ProdutoRepositorioMysql implements RepositorioInterface
{
public function __construct(private readonly \PDO $pdo) {}
public function buscarPorId(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM produtos WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
public function buscarTodos(): array
{
return $this->pdo->query("SELECT * FROM produtos")->fetchAll(\PDO::FETCH_ASSOC);
}
public function salvar(array $dados): int
{
$stmt = $this->pdo->prepare("INSERT INTO produtos (nome, preco) VALUES (?, ?)");
$stmt->execute([$dados['nome'], $dados['preco']]);
return (int) $this->pdo->lastInsertId();
}
public function deletar(int $id): bool
{
$stmt = $this->pdo->prepare("DELETE FROM produtos WHERE id = ?");
$stmt->execute([$id]);
return $stmt->rowCount() > 0;
}
}
// Implementação para testes — em memória, sem banco de dados
class ProdutoRepositorioEmMemoria implements RepositorioInterface
{
private array $dados = [];
private int $proximoId = 1;
public function buscarPorId(int $id): ?array
{
return $this->dados[$id] ?? null;
}
public function buscarTodos(): array
{
return array_values($this->dados);
}
public function salvar(array $dado): int
{
$id = $this->proximoId++;
$this->dados[$id] = array_merge($dado, ['id' => $id]);
return $id;
}
public function deletar(int $id): bool
{
if (!isset($this->dados[$id])) return false;
unset($this->dados[$id]);
return true;
}
}
// O serviço depende da INTERFACE, não de nenhuma implementação concreta
// Em produção recebe ProdutoRepositorioMysql
// Em testes recebe ProdutoRepositorioEmMemoria
// O código do serviço não muda — só a implementação injetada muda
class ProdutoService
{
public function __construct(
private readonly RepositorioInterface $repositorio,
) {}
public function listar(): array
{
return $this->repositorio->buscarTodos();
}
public function criar(string $nome, float $preco): int
{
if (empty($nome)) {
throw new \InvalidArgumentException("Nome do produto não pode ser vazio.");
}
return $this->repositorio->salvar(['nome' => $nome, 'preco' => $preco]);
}
}
Covariância e contravariância
PHP 7.4+ suporta tipos covariantes em retornos e contravariantes em parâmetros. Esses conceitos governam como os tipos podem variar quando uma subclasse ou implementação sobrescreve métodos da interface pai.
Covariância (retorno): uma classe filha pode retornar um tipo mais específico que o declarado na interface/pai. Se a interface declara buscar(): Animal, a implementação pode retornar buscar(): Cachorro — desde que Cachorro extends Animal.
Contravariância (parâmetro): uma classe filha pode aceitar um tipo mais amplo que o declarado na interface/pai. Se a interface declara alimentar(Cachorro $c), a implementação pode aceitar alimentar(Animal $a) — mais permissiva, nunca mais restritiva.
<?php
declare(strict_types=1);
// Hierarquia de tipos para demonstrar covariância/contravariância
abstract class Animal { abstract public function som(): string; }
class Cachorro extends Animal { public function som(): string { return "Au!"; } }
class Gato extends Animal { public function som(): string { return "Miau!"; } }
// Interface base
interface FabricaAnimalInterface
{
public function criar(): Animal; // retorno: Animal
public function alimentar(Animal $a): void; // parâmetro: Animal
}
// ── COVARIÂNCIA — retorno mais específico ──────────────────────────────
// PHP 7.4+: implementação pode retornar tipo mais específico que a interface
class FabricaCachorro implements FabricaAnimalInterface
{
// ✅ Covariante: Cachorro é mais específico que Animal (ok)
public function criar(): Cachorro
{
return new Cachorro();
}
// ✅ Contravariante: Animal é mais amplo que Cachorro (ok)
// Esta fábrica aceita qualquer Animal — não só Cachorro
public function alimentar(Animal $a): void
{
echo "Alimentando " . get_class($a) . ": " . $a->som() . "\n";
}
}
class FabricaGato implements FabricaAnimalInterface
{
// ✅ Covariante: Gato é mais específico que Animal
public function criar(): Gato
{
return new Gato();
}
public function alimentar(Animal $a): void
{
echo "Alimentando " . get_class($a) . ": " . $a->som() . "\n";
}
}
// O cliente usa FabricaAnimalInterface — não sabe qual fábrica concreta é
function criarEAlimentar(FabricaAnimalInterface $fabrica): void
{
$animal = $fabrica->criar();
$fabrica->alimentar($animal);
}
criarEAlimentar(new FabricaCachorro()); // Alimentando Cachorro: Au!
criarEAlimentar(new FabricaGato()); // Alimentando Gato: Miau!
Na prática, covariância é muito comum ao trabalhar com repositórios e factories. Uma RepositorioUsuarioInterface pode declarar buscarPorId(): ?Usuario onde a implementação concreta retorna ?UsuarioAtivo — uma subclasse de Usuario.
Interface Segregation Principle — ISP
O "I" do SOLID diz: um cliente não deve ser forçado a depender de métodos que não usa. Uma interface grande demais obriga implementações a definir métodos vazios ou lançar UnsupportedOperationException — sinal claro de violação do ISP.
A solução é compor comportamentos a partir de interfaces pequenas e focadas:
<?php
declare(strict_types=1);
// ❌ Interface gorda — viola ISP
// Um repositório somente leitura é forçado a implementar salvar() e deletar()
interface RepositorioGordoInterface
{
public function buscarPorId(int $id): ?array;
public function buscarTodos(): array;
public function salvar(array $dados): int;
public function deletar(int $id): bool;
public function paginar(int $pagina, int $porPagina): array;
public function buscarPorFiltro(array $filtros): array;
public function contarTotal(): int;
}
// ✅ Interfaces segregadas — cada cliente usa só o que precisa
interface LegivelInterface
{
public function buscarPorId(int $id): ?array;
public function buscarTodos(): array;
}
interface GravavelInterface
{
public function salvar(array $dados): int;
public function deletar(int $id): bool;
}
interface PaginavelInterface
{
public function paginar(int $pagina, int $porPagina): array;
public function contarTotal(): int;
}
interface FiltravelInterface
{
public function buscarPorFiltro(array $filtros): array;
}
// Repositório completo implementa todas as interfaces necessárias
class ProdutoRepositorio implements LegivelInterface, GravavelInterface, PaginavelInterface, FiltravelInterface
{
// implementa todos os métodos
public function buscarPorId(int $id): ?array { return null; }
public function buscarTodos(): array { return []; }
public function salvar(array $dados): int { return 1; }
public function deletar(int $id): bool { return true; }
public function paginar(int $p, int $pp): array { return []; }
public function contarTotal(): int { return 0; }
public function buscarPorFiltro(array $f): array { return []; }
}
// Repositório somente leitura — implementa apenas o necessário
class RelatorioRepositorio implements LegivelInterface, PaginavelInterface, FiltravelInterface
{
public function buscarPorId(int $id): ?array { return null; }
public function buscarTodos(): array { return []; }
public function paginar(int $p, int $pp): array { return []; }
public function contarTotal(): int { return 0; }
public function buscarPorFiltro(array $f): array { return []; }
// ✅ Não implementa salvar() nem deletar() — não precisa
}
// Serviços dependem apenas das interfaces que realmente usam
class ExportadorRelatorio
{
// Precisa ler e paginar — não precisa escrever
public function __construct(
private readonly LegivelInterface&PaginavelInterface $repositorio,
) {}
}
class AdministracaoProdutos
{
// Precisa de tudo
public function __construct(
private readonly LegivelInterface&GravavelInterface $repositorio,
) {}
}
A sintaxe LegivelInterface&PaginavelInterface é uma intersection type do PHP 8.1 — o parâmetro aceita apenas objetos que implementam ambas as interfaces simultaneamente.
Interfaces com constantes e herança de interfaces
Interfaces podem declarar constantes (implicitamente public final) e podem estender outras interfaces — inclusive múltiplas ao mesmo tempo. Isso permite construir hierarquias de contratos sem herança de implementação.
<?php
declare(strict_types=1);
// Interface com constantes — úteis para valores do domínio
interface StatusPedidoInterface
{
const PENDENTE = 'pendente';
const APROVADO = 'aprovado';
const CANCELADO = 'cancelado';
const ENTREGUE = 'entregue';
public function status(): string;
public function podeSerCancelado(): bool;
}
// Interface estendendo outra — herança de contrato
interface PedidoRastreavel extends StatusPedidoInterface
{
public function codigoRastreamento(): ?string;
public function atualizarLocalizacao(string $local): void;
}
// Interface estendendo múltiplas — composição de contratos
interface PedidoCompletoInterface extends StatusPedidoInterface, PedidoRastreavel
{
public function valorTotal(): float;
public function itens(): array;
}
// Uma classe que implementa PedidoCompletoInterface
// é obrigada a implementar TODOS os métodos das três interfaces
class Pedido implements PedidoCompletoInterface
{
private string $status = StatusPedidoInterface::PENDENTE;
private ?string $rastreamento = null;
private array $historico = [];
public function __construct(
private readonly array $itens,
private readonly float $total,
) {}
public function status(): string { return $this->status; }
public function valorTotal(): float { return $this->total; }
public function itens(): array { return $this->itens; }
public function codigoRastreamento(): ?string { return $this->rastreamento; }
public function podeSerCancelado(): bool
{
return in_array($this->status, [
StatusPedidoInterface::PENDENTE,
StatusPedidoInterface::APROVADO,
]);
}
public function atualizarLocalizacao(string $local): void
{
$this->historico[] = ['local' => $local, 'em' => date('Y-m-d H:i:s')];
echo "📍 Pedido em: {$local}\n";
}
}
Interfaces funcionais e Closures
PHP não tem interfaces funcionais como Java (interfaces com um único método abstrato, usadas como lambdas), mas o padrão existe implicitamente: qualquer interface com um único método pode ser substituída por uma Closure quando a flexibilidade for mais importante que a nomeação explícita.
<?php
declare(strict_types=1);
// Interface funcional — um único método abstrato
interface TransformadorInterface
{
public function transformar(mixed $valor): mixed;
}
// Implementação com classe — útil quando tem estado ou precisa ser injetada
class UpperCaseTransformador implements TransformadorInterface
{
public function transformar(mixed $valor): mixed
{
return is_string($valor) ? strtoupper($valor) : $valor;
}
}
// Pipeline que aceita a interface
class Pipeline
{
/** @var TransformadorInterface[] */
private array $etapas = [];
public function pipe(TransformadorInterface $t): static
{
$this->etapas[] = $t;
return $this;
}
// Também aceita Closure — adaptador inline para a interface
public function pipeCallback(callable $fn): static
{
// Closure adaptada para a interface — padrão Adapter em miniatura
return $this->pipe(new class($fn) implements TransformadorInterface {
public function __construct(private readonly \Closure $fn) {}
public function transformar(mixed $valor): mixed
{
return ($this->fn)($valor);
}
});
}
public function processar(mixed $entrada): mixed
{
return array_reduce(
$this->etapas,
fn($carry, TransformadorInterface $t) => $t->transformar($carry),
$entrada
);
}
}
$resultado = (new Pipeline())
->pipe(new UpperCaseTransformador())
->pipeCallback(fn($v) => trim($v))
->pipeCallback(fn($v) => str_replace(' ', '-', $v))
->processar(" olá mundo ");
echo $resultado . "\n";
// OLÁ-MUNDO
Interface boa é a que você consegue implementar sem consultar o código de quem a definiu. Isso empurra para poucos métodos, nomes que descrevem intenção e tipos que não exigem conhecimento interno — e explica por que dividir um contrato inchado costuma melhorar o sistema inteiro, e não apenas a classe que o implementava a contragosto. Vale a ressalva de sempre: contrato demais também atrapalha, e interface com uma implementação só, criada "por precaução", é indireção que ainda não comprou nada.
Fontes e leituras recomendadas
- PHP Manual — Interfaces: https://www.php.net/manual/pt_BR/language.oop5.interfaces.php
- PHP Manual — Declarações de tipo: https://www.php.net/manual/pt_BR/language.types.declarations.php
- PHP Manual — Covariance and Contravariance: https://www.php.net/manual/pt_BR/language.oop5.variance.php
- MARTIN, R.C. — Interface Segregation Principle. https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/isp.pdf
- Refactoring Guru — o que é um padrão de projeto: https://refactoring.guru/pt-br/design-patterns/what-is-pattern
- PHP: The Right Way — Design Patterns: https://phptherightway.com/#design_patterns
Exercícios
Exercício 1
Crie as interfaces LogavelInterface (com log(string $nivel, string $mensagem): void), FormatavelInterface (com formatar(array $dados): string) e EnviavelInterface (com enviar(string $destino, string $conteudo): bool). Implemente um Notificador que recebe as três interfaces e as combina para logar, formatar e enviar notificações.
Ver resposta
✓ Resposta: Três interfaces pequenas em vez de uma NotificadorInterface gorda: cada uma varia por um motivo diferente — formato, transporte, destino do log. Quem implementa só precisa do que usa.
<?php
declare(strict_types=1);
interface LogavelInterface
{
public function log(string $nivel, string $mensagem): void;
}
interface FormatavelInterface
{
public function formatar(array $dados): string;
}
interface EnviavelInterface
{
public function enviar(string $destino, string $conteudo): bool;
}
// ---------- Implementações intercambiáveis --------------------------------
final class LogArquivo implements LogavelInterface
{
public function __construct(private readonly string $caminho) {}
public function log(string $nivel, string $mensagem): void
{
file_put_contents(
$this->caminho,
sprintf("[%s] %s %s\n", strtoupper($nivel),
(new DateTimeImmutable())->format('c'), $mensagem),
FILE_APPEND | LOCK_EX,
);
}
}
final class FormatadorTexto implements FormatavelInterface
{
public function formatar(array $dados): string
{
$linhas = [];
foreach ($dados as $chave => $valor) {
$linhas[] = ucfirst((string) $chave) . ': '
. (is_scalar($valor) ? (string) $valor : json_encode($valor));
}
return implode(PHP_EOL, $linhas);
}
}
final class FormatadorHtml implements FormatavelInterface
{
public function formatar(array $dados): string
{
$itens = array_map(
static fn($c, $v): string => sprintf(
'<li><strong>%s:</strong> %s</li>',
htmlspecialchars((string) $c, ENT_QUOTES),
htmlspecialchars((string) $v, ENT_QUOTES),
),
array_keys($dados),
$dados,
);
return '<ul>' . implode('', $itens) . '</ul>';
}
}
final class EnvioEmail implements EnviavelInterface
{
public function enviar(string $destino, string $conteudo): bool
{
if (!filter_var($destino, FILTER_VALIDATE_EMAIL)) {
return false;
}
return true; // aqui entraria o mailer de verdade
}
}
// ---------- O Notificador só conhece os contratos -------------------------
final class Notificador
{
public function __construct(
private readonly LogavelInterface $logger,
private readonly FormatavelInterface $formatador,
private readonly EnviavelInterface $transporte,
) {}
public function notificar(string $destino, array $dados): bool
{
$this->logger->log('info', "Preparando notificação para {$destino}");
$conteudo = $this->formatador->formatar($dados);
$ok = $this->transporte->enviar($destino, $conteudo);
$this->logger->log($ok ? 'info' : 'error',
$ok ? "Enviado para {$destino}" : "Falha ao enviar para {$destino}");
return $ok;
}
}
// Trocar e-mail por SMS, ou texto por HTML, não muda uma linha do Notificador.
$notificador = new Notificador(
new LogArquivo('/tmp/notificacoes.log'),
new FormatadorHtml(),
new EnvioEmail(),
);
$notificador->notificar('cliente@exemplo.com', [
'pedido' => 'PED-1042',
'status' => 'Pago',
'total' => 'R$ 349,90',
]);
Exercício 2
Demonstre covariância: crie uma interface CriadorInterface com criar(): Veiculo. Implemente CriadorCarro retornando Carro extends Veiculo e CriadorMoto retornando Moto extends Veiculo. Verifique que o código cliente usando CriadorInterface funciona com ambas sem modificação.
Ver resposta
✓ Resposta: Covariância vale para o retorno; nos parâmetros a regra se inverte (contravariância) — uma implementação pode aceitar tipo mais genérico, nunca mais específico. Faz sentido pela substituição: quem programa contra a interface tem de conseguir passar qualquer coisa que ela permita.
<?php
declare(strict_types=1);
abstract class Veiculo
{
abstract public function rodas(): int;
public function descricao(): string
{
return static::class . ' com ' . $this->rodas() . ' rodas';
}
}
final class Carro extends Veiculo
{
public function rodas(): int { return 4; }
public function abrirPortaMalas(): string { return 'Porta-malas aberto'; }
}
final class Moto extends Veiculo
{
public function rodas(): int { return 2; }
public function empinar(): string { return 'Wheelie!'; }
}
interface CriadorInterface
{
public function criar(): Veiculo;
}
final class CriadorCarro implements CriadorInterface
{
// Covariância: o retorno declarado é MAIS específico que o da interface.
// Isso é permitido desde o PHP 7.4 — e é seguro, porque todo Carro é um
// Veiculo. O contrário (devolver algo mais genérico) seria erro fatal.
public function criar(): Carro
{
return new Carro();
}
}
final class CriadorMoto implements CriadorInterface
{
public function criar(): Moto
{
return new Moto();
}
}
// ---------- Cliente genérico: não muda para nenhum criador novo -----------
function exibirVeiculo(CriadorInterface $criador): void
{
// Aqui o tipo estático é Veiculo — é só o que o contrato promete.
$veiculo = $criador->criar();
echo $veiculo->descricao(), PHP_EOL;
}
foreach ([new CriadorCarro(), new CriadorMoto()] as $criador) {
exibirVeiculo($criador);
}
// Carro com 4 rodas
// Moto com 2 rodas
// ---------- O ganho da covariância ---------------------------------------
$criadorCarro = new CriadorCarro();
// Como criar() declara `: Carro`, isto é válido sem cast nem instanceof.
echo $criadorCarro->criar()->abrirPortaMalas(), PHP_EOL;
// Fosse `: Veiculo`, a linha acima seria erro estático — Veiculo não tem
// abrirPortaMalas(). Seria preciso um instanceof só para convencer o
// analisador do que você já sabia.
Exercício 3
Refatore um repositório gordo em interfaces segregadas: BuscavelInterface, SalvavelInterface, DeletavelInterface. Crie um repositório de auditoria que implementa apenas BuscavelInterface — sem métodos desnecessários.
Ver resposta
✓ Resposta: O sinal de que uma interface precisa ser fatiada é justamente o método implementado com throw new BadMethodCallException. Isso quebra o princípio de substituição: quem recebe o tipo espera poder chamar tudo.
<?php
declare(strict_types=1);
// ---------- ANTES: a interface gorda --------------------------------------
// interface RepositorioInterface {
// public function buscar(int $id): ?object;
// public function todos(): array;
// public function salvar(object $e): void;
// public function deletar(int $id): void;
// }
//
// O repositório de auditoria é só leitura — implementá-la obrigaria a
// escrever salvar() e deletar() com `throw new BadMethodCallException`.
// Método que existe só para explodir é sinal de interface errada.
// ---------- DEPOIS: três contratos independentes --------------------------
interface BuscavelInterface
{
public function buscar(int $id): ?object;
/** @return object[] */
public function todos(): array;
}
interface SalvavelInterface
{
public function salvar(object $entidade): int;
}
interface DeletavelInterface
{
public function deletar(int $id): bool;
}
// Quem precisa de tudo, compõe — inclusive numa interface de conveniência.
interface RepositorioCompletoInterface extends
BuscavelInterface, SalvavelInterface, DeletavelInterface
{
}
final class ProdutoRepositorio implements RepositorioCompletoInterface
{
public function __construct(private readonly PDO $pdo) {}
public function buscar(int $id): ?object
{
$stmt = $this->pdo->prepare('SELECT * FROM produtos WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_OBJ) ?: null;
}
public function todos(): array
{
return $this->pdo->query('SELECT * FROM produtos ORDER BY nome')
->fetchAll(PDO::FETCH_OBJ);
}
public function salvar(object $entidade): int
{
$stmt = $this->pdo->prepare(
'INSERT INTO produtos (nome, preco) VALUES (:nome, :preco)'
);
$stmt->execute([':nome' => $entidade->nome, ':preco' => $entidade->preco]);
return (int) $this->pdo->lastInsertId();
}
public function deletar(int $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM produtos WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
}
// Auditoria é imutável por natureza: implementa SÓ o que faz sentido.
final class AuditoriaRepositorio implements BuscavelInterface
{
public function __construct(private readonly PDO $pdo) {}
public function buscar(int $id): ?object
{
$stmt = $this->pdo->prepare('SELECT * FROM auditoria WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_OBJ) ?: null;
}
public function todos(): array
{
return $this->pdo->query('SELECT * FROM auditoria ORDER BY criado_em DESC')
->fetchAll(PDO::FETCH_OBJ);
}
}
// O tipo do parâmetro documenta a intenção: esta função só lê.
function exportarCsv(BuscavelInterface $fonte): string
{
$linhas = array_map(
static fn(object $r): string => implode(',', (array) $r),
$fonte->todos(),
);
return implode(PHP_EOL, $linhas);
}
echo exportarCsv(new AuditoriaRepositorio($pdo));
echo exportarCsv(new ProdutoRepositorio($pdo)); // também serve: implementa Buscavel
Exercício 4
Construa um mini-pipeline de processamento de pedidos usando a interface EtapaPedidoInterface com processar(array $pedido): array. Implemente três etapas: ValidarEstoque, CalcularFrete, AplicarDesconto. O pipeline executa todas em sequência e retorna o pedido processado.
Ver resposta
✓ Resposta: Etapa nova é classe nova mais uma linha no array — o PipelinePedido nunca muda. O cuidado real fica na ordem: AplicarDesconto zera o frete, então precisa rodar depois de CalcularFrete. Essa dependência é implícita, e vale documentá-la.
<?php
declare(strict_types=1);
interface EtapaPedidoInterface
{
public function processar(array $pedido): array;
public function nome(): string;
}
final class PedidoInvalidoException extends RuntimeException
{
}
final class ValidarEstoque implements EtapaPedidoInterface
{
/** @param array<string, int> $estoque */
public function __construct(private readonly array $estoque) {}
public function processar(array $pedido): array
{
foreach ($pedido['itens'] as $item) {
$disponivel = $this->estoque[$item['sku']] ?? 0;
if ($item['quantidade'] > $disponivel) {
throw new PedidoInvalidoException(
"Sem estoque de {$item['sku']}: pedidas {$item['quantidade']}, "
. "há {$disponivel}."
);
}
}
$pedido['estoque_validado'] = true;
return $pedido;
}
public function nome(): string { return 'validar-estoque'; }
}
final class CalcularFrete implements EtapaPedidoInterface
{
public function processar(array $pedido): array
{
$peso = array_sum(array_map(
static fn(array $i): float => $i['peso'] * $i['quantidade'],
$pedido['itens'],
));
$pedido['peso_total'] = $peso;
$pedido['frete'] = round(14.90 + $peso * 2.30, 2);
return $pedido;
}
public function nome(): string { return 'calcular-frete'; }
}
final class AplicarDesconto implements EtapaPedidoInterface
{
public function __construct(
private readonly float $minimoFreteGratis = 300.00,
) {}
public function processar(array $pedido): array
{
$subtotal = array_sum(array_map(
static fn(array $i): float => $i['preco'] * $i['quantidade'],
$pedido['itens'],
));
$pedido['subtotal'] = $subtotal;
// Depende do frete já calculado — por isso esta etapa vem depois.
if ($subtotal >= $this->minimoFreteGratis) {
$pedido['desconto'] = $pedido['frete'] ?? 0.0;
$pedido['frete'] = 0.0;
}
$pedido['total'] = $subtotal + ($pedido['frete'] ?? 0.0);
return $pedido;
}
public function nome(): string { return 'aplicar-desconto'; }
}
final class PipelinePedido
{
/** @param EtapaPedidoInterface[] $etapas */
public function __construct(private readonly array $etapas) {}
public function executar(array $pedido): array
{
foreach ($this->etapas as $etapa) {
// A saída de uma é a entrada da próxima. Como cada etapa recebe e
// devolve o pedido inteiro, dá para reordenar sem mexer nelas —
// desde que a dependência de dados seja respeitada.
$pedido = $etapa->processar($pedido);
}
return $pedido;
}
}
$pipeline = new PipelinePedido([
new ValidarEstoque(['TEC-01' => 10, 'MOU-02' => 4]),
new CalcularFrete(),
new AplicarDesconto(),
]);
try {
$resultado = $pipeline->executar([
'cliente' => 'Ana',
'itens' => [
['sku' => 'TEC-01', 'quantidade' => 1, 'preco' => 349.90, 'peso' => 1.2],
['sku' => 'MOU-02', 'quantidade' => 2, 'preco' => 129.50, 'peso' => 0.3],
],
]);
printf("Subtotal: R$ %.2f | Frete: R$ %.2f | Total: R$ %.2f%s",
$resultado['subtotal'], $resultado['frete'], $resultado['total'], PHP_EOL);
// Subtotal: R$ 608.90 | Frete: R$ 0.00 | Total: R$ 608.90
} catch (PedidoInvalidoException $e) {
echo '✗ ', $e->getMessage(), PHP_EOL;
}
Exercício 5
Desafio: use intersection types para criar um RelatorioService que aceita LegivelInterface&FiltravelInterface&PaginavelInterface. Implemente duas classes que satisfazem essas interfaces de formas diferentes (uma com array em memória, outra simulando consulta ao banco) e verifique que ambas podem ser injetadas no serviço.
Ver resposta
✓ Resposta: Não confunda A&B com A|B: o & exige as duas capacidades, o | aceita qualquer uma. E repare no : static de filtrar() — é ele que permite encadear filtrar()->pagina()->ler() mantendo o tipo concreto.
<?php
declare(strict_types=1);
interface LegivelInterface
{
/** @return array<int, array<string, mixed>> */
public function ler(): array;
}
interface FiltravelInterface
{
public function filtrar(callable $criterio): static;
}
interface PaginavelInterface
{
public function pagina(int $pagina, int $porPagina): static;
public function total(): int;
}
// ---------- Fonte 1: array em memória -------------------------------------
final class FonteMemoria implements LegivelInterface, FiltravelInterface, PaginavelInterface
{
public function __construct(
private readonly array $dados,
private readonly ?int $pagina = null,
private readonly int $porPagina = 20,
) {}
public function ler(): array
{
if ($this->pagina === null) {
return array_values($this->dados);
}
return array_slice(
array_values($this->dados),
($this->pagina - 1) * $this->porPagina,
$this->porPagina,
);
}
// Devolve nova instância: a fonte é imutável, encadear não destrói a
// anterior. É o que `static` no retorno permite tipar corretamente.
public function filtrar(callable $criterio): static
{
return new static(array_filter($this->dados, $criterio),
$this->pagina, $this->porPagina);
}
public function pagina(int $pagina, int $porPagina): static
{
return new static($this->dados, max(1, $pagina), max(1, $porPagina));
}
public function total(): int
{
return count($this->dados);
}
}
// ---------- Fonte 2: consulta ao banco (montada, não executada até ler) ---
final class FonteBanco implements LegivelInterface, FiltravelInterface, PaginavelInterface
{
public function __construct(
private readonly PDO $pdo,
private readonly string $tabela,
private readonly array $condicoes = [],
private readonly ?int $pagina = null,
private readonly int $porPagina = 20,
) {}
public function ler(): array
{
$sql = "SELECT * FROM {$this->tabela}";
if ($this->condicoes !== []) {
$sql .= ' WHERE ' . implode(' AND ', array_keys($this->condicoes));
}
if ($this->pagina !== null) {
$sql .= sprintf(' LIMIT %d OFFSET %d',
$this->porPagina, ($this->pagina - 1) * $this->porPagina);
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute(array_values($this->condicoes));
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Aqui "filtrar" vira SQL, não array_filter — mesma interface, mecânica
// completamente diferente. É esse o ponto do exercício.
public function filtrar(callable $criterio): static
{
[$expressao, $valor] = $criterio();
return new static($this->pdo, $this->tabela,
[...$this->condicoes, $expressao => $valor],
$this->pagina, $this->porPagina);
}
public function pagina(int $pagina, int $porPagina): static
{
return new static($this->pdo, $this->tabela, $this->condicoes,
max(1, $pagina), max(1, $porPagina));
}
public function total(): int
{
$sql = "SELECT COUNT(*) FROM {$this->tabela}";
if ($this->condicoes !== []) {
$sql .= ' WHERE ' . implode(' AND ', array_keys($this->condicoes));
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute(array_values($this->condicoes));
return (int) $stmt->fetchColumn();
}
}
// ---------- O serviço exige as TRÊS capacidades ao mesmo tempo ------------
final class RelatorioService
{
// Intersection type (PHP 8.1): o argumento tem de satisfazer todas as
// interfaces. Com `|` seria "qualquer uma delas" — o oposto do que se quer.
public function __construct(
private readonly LegivelInterface&FiltravelInterface&PaginavelInterface $fonte,
) {}
public function gerar(callable $criterio, int $pagina = 1, int $porPagina = 20): array
{
$filtrada = $this->fonte->filtrar($criterio);
$linhas = $filtrada->pagina($pagina, $porPagina)->ler();
return [
'linhas' => $linhas,
'total' => $filtrada->total(),
'pagina' => $pagina,
'paginas' => (int) ceil($filtrada->total() / $porPagina),
];
}
}
// As duas fontes entram no mesmo serviço, sem adaptador no meio.
$memoria = new FonteMemoria([
['produto' => 'Teclado', 'total' => 34990],
['produto' => 'Mouse', 'total' => 12950],
['produto' => 'Monitor', 'total' => 129900],
]);
$relatorio = new RelatorioService($memoria);
print_r($relatorio->gerar(
static fn(array $l): bool => $l['total'] > 20000,
pagina: 1, porPagina: 10,
));
// linhas: Teclado e Monitor | total: 2 | paginas: 1
$banco = new FonteBanco($pdo, 'vendas');
$relatorioBanco = new RelatorioService($banco); // mesmo serviço, outra fonte