Interfaces Avançadas

Interfaces Avançadas

Interface é contrato, e contrato tem regras que vão além de listar métodos. Covariância e contravariância no retorno e nos parâmetros, o princípio da segregação que explica por que interface grande atrapalha, constantes e herança entre interfaces, e o ponto em que uma closure já cumpre o papel.
PHP

22 min de leitura

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

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
Comentários

Mais em PHP

O que é PHP e por que ele ainda importa
O que é PHP e por que ele ainda importa

PHP roda no servidor, e é essa única característica que explica quase tudo…

Observer, Decorator e Strategy
Observer, Decorator e Strategy

Três padrões que aparecem em praticamente qualquer sistema: o Observer, que…

Dominando o PHP
Dominando o PHP

A série começa pelo terreno: o que a linguagem é hoje, onde ela realmente roda…