Design Patterns (Padrões de Projeto) são soluções codificadas para problemas que aparecem repetidamente no design de software orientado a objetos. O livro Design Patterns (GoF, 1994) catalogou 23 padrões em três categorias: criacionais (como criar objetos), estruturais (como compor objetos) e comportamentais (como objetos comunicam).
Neste artigo cobrimos os três padrões criacionais mais usados em PHP: Singleton (garante uma única instância), Factory Method (delega a criação a subclasses) e Builder (constrói objetos complexos passo a passo). Você os encontrará diretamente em Laravel, Symfony e Doctrine.
Os três padrões — visão geral
Singleton — uma única instância
O Singleton garante que uma classe tenha exatamente uma instância durante toda a execução, e fornece um ponto de acesso global a ela. O caso de uso mais clássico é a conexão de banco de dados — abrir uma nova conexão a cada chamada seria caro e desnecessário. O padrão usa três elementos: construtor privado (impede new externo), propriedade estática para guardar a instância, e método estático getInstance() que cria ou retorna a instância existente.
<?php declare(strict_types=1); namespace MeuApp\Database; use PDO; use PDOException; final class Conexao { // A instância única fica guardada nesta propriedade estática private static ?self $instancia = null; private PDO $pdo; // Construtor privado — nenhum código externo pode chamar "new Conexao()" private function __construct( string $dsn, string $usuario, string $senha, ) { try { $this->pdo = new PDO($dsn, $usuario, $senha, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { throw new \RuntimeException("Falha ao conectar ao banco: " . $e->getMessage()); } } // Método estático — único ponto de acesso à instância public static function getInstance( string $dsn = "mysql:host=localhost;dbname=app;charset=utf8mb4", string $usuario = "root", string $senha = "", ): static { // Cria a instância apenas na primeira chamada if (self::$instancia === null) { self::$instancia = new self($dsn, $usuario, $senha); } return self::$instancia; } // Impede clonagem — mantém a unicidade private function __clone(): void {} public function pdo(): PDO { return $this->pdo; } public function buscarTodos(string $tabela): array { return $this->pdo ->query("SELECT * FROM {$tabela}") ->fetchAll(); } } // Uso — todas as chamadas retornam a MESMA instância $db1 = Conexao::getInstance(); $db2 = Conexao::getInstance(); var_dump($db1 === $db2); // bool(true) — mesma instância na memória $produtos = Conexao::getInstance()->buscarTodos("produtos");
O Singleton é frequentemente apontado como um antipadrão porque cria estado global, dificulta testes unitários (não dá para substituir por um mock facilmente) e introduz acoplamento oculto. Em PHP moderno, a prática preferida é injeção de dependência — um container DI (como o do Laravel ou Symfony) gerencia a criação e o ciclo de vida dos objetos. Use Singleton com consciência: é aceitável para conexões de banco, loggers e configurações; evite para lógica de negócio.
Factory Method — delegar a criação
O Factory Method define um método para criar objetos, mas deixa as subclasses (ou uma classe factory dedicada) decidirem qual classe concreta instanciar. O cliente passa a depender de uma interface ou classe abstrata, não de implementações específicas. Isso elimina condicionais if/switch espalhados pelo código toda vez que você precisa criar um objeto diferente com base em um parâmetro.
O código cliente precisa conhecer todas as classes concretas e decidir qual instanciar. Cada novo tipo de notificação exige alterar este código.
O cliente pede uma notificação para a factory e recebe um objeto que implementa a interface. Adicionar WhatsApp não altera o cliente.
<?php declare(strict_types=1); namespace MeuApp\Notificacoes; use InvalidArgumentException; // Interface — o contrato que toda notificação deve cumprir interface NotificacaoInterface { public function enviar(string $destinatario, string $mensagem): bool; public function canal(): string; } // Implementações concretas class NotificacaoEmail implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { // Em produção: chamar um provedor como Mailgun, SendGrid, SES echo "📧 Email → {$dest}: {$msg}\n"; return true; } public function canal(): string { return "email"; } } class NotificacaoSMS implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { // Em produção: chamar Twilio, AWS SNS, Vonage echo "📱 SMS → {$dest}: {$msg}\n"; return true; } public function canal(): string { return "sms"; } } class NotificacaoSlack implements NotificacaoInterface { public function enviar(string $dest, string $msg): bool { echo "💬 Slack → #{$dest}: {$msg}\n"; return true; } public function canal(): string { return "slack"; } } // Factory — cria a implementação correta com base em uma string // O cliente não precisa conhecer nenhuma classe concreta final class NotificacaoFactory { // Mapa de tipo → classe — para adicionar WhatsApp: uma linha aqui private const TIPOS = [ "email" => NotificacaoEmail::class, "sms" => NotificacaoSMS::class, "slack" => NotificacaoSlack::class, ]; public static function criar(string $tipo): NotificacaoInterface { if (!isset(self::TIPOS[$tipo])) { throw new InvalidArgumentException( "Canal desconhecido: '{$tipo}'. Disponíveis: " . implode(", ", array_keys(self::TIPOS)) ); } $classe = self::TIPOS[$tipo]; return new $classe(); } // Variante — cria múltiplos canais de uma vez public static function criarMultiplos(string ...$tipos): array { return array_map(self::criar(...), $tipos); } } // Uso — o cliente só conhece a interface e a factory $canais = NotificacaoFactory::criarMultiplos("email", "sms", "slack"); foreach ($canais as $canal) { $canal->enviar("ana@email.com", "Pedido confirmado!"); } // 📧 Email → ana@email.com: Pedido confirmado! // 📱 SMS → ana@email.com: Pedido confirmado! // 💬 Slack → #ana@email.com: Pedido confirmado! // Lendo canal preferido de uma config, banco de dados, etc. $canalPreferido = "sms"; // viria de $_ENV["NOTIFICACAO_CANAL"] $n = NotificacaoFactory::criar($canalPreferido); echo $n->canal(); // sms
Abstract Factory — fábrica de famílias
Quando você precisa criar famílias inteiras de objetos relacionados — por exemplo, um conjunto consistente de componentes de UI para diferentes temas — o Abstract Factory estende o conceito. Cada factory concreta produz objetos compatíveis entre si:
<?php declare(strict_types=1); // Abstract Factory — cria famílias de objetos relacionados // Exemplo: diferentes drivers de armazenamento com API consistente interface StorageFactory { public function criarLeitor(): Leitor; public function criarEscritor(): Escritor; } interface Leitor { public function ler(string $chave): mixed; } interface Escritor { public function escrever(string $chave, mixed $valor): void; } // Família Redis class RedisLeitor implements Leitor { public function ler(string $k): mixed { return "[Redis:get {$k}]"; } } class RedisEscritor implements Escritor { public function escrever(string $k, mixed $v): void { echo "[Redis:set {$k}={$v}]\n"; } } // Família Arquivo class ArquivoLeitor implements Leitor { public function ler(string $k): mixed { return "[File:read {$k}]"; } } class ArquivoEscritor implements Escritor { public function escrever(string $k, mixed $v): void { echo "[File:write {$k}={$v}]\n"; } } class RedisFactory implements StorageFactory { public function criarLeitor(): Leitor { return new RedisLeitor(); } public function criarEscritor(): Escritor { return new RedisEscritor(); } } class ArquivoFactory implements StorageFactory { public function criarLeitor(): Leitor { return new ArquivoLeitor(); } public function criarEscritor(): Escritor { return new ArquivoEscritor(); } } // Código cliente usa só a interface — pode trocar Redis por Arquivo // sem modificar uma linha desta função function executarCache(StorageFactory $factory): void { $escritor = $factory->criarEscritor(); $leitor = $factory->criarLeitor(); $escritor->escrever("sessao", "abc123"); echo $leitor->ler("sessao") . "\n"; } executarCache(new RedisFactory()); // [Redis:set sessao=abc123] // [Redis:get sessao] executarCache(new ArquivoFactory()); // [File:write sessao=abc123] // [File:read sessao]
Builder — construindo objetos complexos
O Builder separa a construção de um objeto complexo da sua representação final. É a solução elegante para o problema do "construtor telescópico" — quando uma classe precisa de muitos parâmetros opcionais, criar um construtor com 10 argumentos é impraticável e propenso a erros (fácil trocar a ordem dos argumentos). O Builder oferece uma API fluente onde cada parâmetro opcional é um método com nome descritivo.
<?php declare(strict_types=1); namespace MeuApp\Mail; // O produto — objeto final imutável que o Builder constrói final class Email { public function __construct( public readonly string $de, public readonly string $para, public readonly string $assunto, public readonly string $corpo, public readonly array $cc = [], public readonly array $bcc = [], public readonly array $anexos = [], public readonly bool $html = false, public readonly ?string $replyTo = null, public readonly int $prioridade = 3, ) {} } // Builder — constrói o Email passo a passo com API fluente final class EmailBuilder { private string $de; private string $para; private string $assunto; private string $corpo; private array $cc = []; private array $bcc = []; private array $anexos = []; private bool $html = false; private ?string $replyTo = null; private int $prioridade = 3; // Construtor factory estático — ponto de entrada semântico public static function novo(string $de, string $para): static { $b = new static(); $b->de = $de; $b->para = $para; return $b; } // Cada método retorna $this — permite encadeamento fluente public function assunto(string $assunto): static { $this->assunto = $assunto; return $this; } public function corpo(string $corpo, bool $html = false): static { $this->corpo = $corpo; $this->html = $html; return $this; } public function cc(string ...$enderecos): static { $this->cc = array_merge($this->cc, $enderecos); return $this; } public function bcc(string ...$enderecos): static { $this->bcc = array_merge($this->bcc, $enderecos); return $this; } public function anexar(string $caminhoArquivo): static { $this->anexos[] = $caminhoArquivo; return $this; } public function replyTo(string $endereco): static { $this->replyTo = $endereco; return $this; } public function urgente(): static { $this->prioridade = 1; // 1 = alta, 3 = normal, 5 = baixa return $this; } // build() — valida e cria o objeto final imutável public function build(): Email { if (empty($this->assunto)) { throw new \InvalidArgumentException("Email precisa de assunto."); } if (empty($this->corpo)) { throw new \InvalidArgumentException("Email precisa de corpo."); } return new Email( $this->de, $this->para, $this->assunto, $this->corpo, $this->cc, $this->bcc, $this->anexos, $this->html, $this->replyTo, $this->prioridade, ); } } // Uso — API fluente legível como uma frase $email = EmailBuilder::novo("sistema@app.com", "cliente@email.com") ->assunto("Confirmação do pedido #1234") ->corpo("<h1>Pedido confirmado!</h1><p>Obrigado pela compra.</p>", true) ->cc("gerente@app.com") ->bcc("auditoria@app.com") ->anexar("/tmp/nota-fiscal-1234.pdf") ->replyTo("suporte@app.com") ->build(); echo $email->assunto; // Confirmação do pedido #1234 var_dump($email->html); // bool(true) var_dump($email->anexos); // ["/tmp/nota-fiscal-1234.pdf"] // Email urgente — só adiciona o que precisa $alerta = EmailBuilder::novo("sistema@app.com", "ops@app.com") ->assunto("🚨 Servidor fora do ar") ->corpo("Disco cheio em prod-01. Ação imediata necessária.") ->urgente() ->build(); var_dump($alerta->prioridade); // int(1)
Builder com Director
Quando certas configurações de Build se repetem muito — como um "email de boas-vindas" ou uma "query de relatório mensal" — um Director encapsula a sequência de chamadas ao Builder, criando um ponto reutilizável para construções pré-definidas:
<?php declare(strict_types=1); use MeuApp\Mail\EmailBuilder; use MeuApp\Mail\Email; // Director — conhece as "receitas" de construção mais comuns final class EmailDirector { public static function boasVindas(string $para, string $nome): Email { return EmailBuilder::novo("noreply@app.com", $para) ->assunto("Bem-vindo ao App, {$nome}!") ->corpo("<h1>Olá, {$nome}!</h1><p>Sua conta foi criada com sucesso.</p>", true) ->replyTo("suporte@app.com") ->build(); } public static function alertaCritico(string $para, string $mensagem): Email { return EmailBuilder::novo("sistema@app.com", $para) ->assunto("🚨 ALERTA CRÍTICO — " . date("d/m H:i")) ->corpo($mensagem) ->bcc("diretoria@app.com") ->urgente() ->build(); } } // Uso — limpo, intencional, sem repetição $emailBv = EmailDirector::boasVindas("joao@email.com", "João"); $emailAlerta = EmailDirector::alertaCritico("ops@app.com", "Memória RAM em 95%");
O QueryBuilder do Laravel (DB::table('users')->where('ativo', true)->orderBy('nome')->get()) é exatamente o padrão Builder. Cada where(), orderBy(), limit() acumula a configuração, e o get() (equivalente ao build()) compila tudo em SQL e executa. O mesmo vale para o QueryBuilder do Doctrine ORM e para clientes HTTP como Guzzle.
Dos três, o Singleton é o único que exige uma ressalva permanente: ele resolve mesmo o problema de instância única, e cria outro maior, o da dependência que ninguém vê na assinatura. Quando o objetivo é economizar conexões ou compartilhar configuração, o contêiner de injeção entrega o mesmo resultado sem o estado global — e é por isso que você vai encontrá-lo em todo framework moderno e não vai encontrar Singletons. Conhecer o padrão continua valendo: ele está em praticamente todo sistema PHP com mais de dez anos, e agora você sabe por onde começar a desmontá-lo.
Fontes e leituras recomendadas
-
Padrões Criacionais — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/creational-patterns
Introdução em português aos cinco padrões criacionais (Singleton, Factory Method, Abstract Factory, Builder e Prototype) com diagramas e exemplos interativos.
-
Singleton — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/singleton
Explicação detalhada com diagrama de classes, implementação em PHP, quando usar, e críticas ao padrão — incluindo por que é considerado um antipadrão em muitos contextos.
-
Builder — Refactoring Guru Padrão https://refactoring.guru/pt-br/design-patterns/builder
Visão completa do padrão Builder: produto, builder concreto, director, e como a API fluente é uma variação moderna do padrão original.
-
GAMMA et al. — Design Patterns (GoF). Addison-Wesley, 1994 Livro https://www.oreilly.com/library/view/design-patterns-elements/0201633612/
A fonte original. Os capítulos de Abstract Factory, Builder e Singleton são referência obrigatória para entender as intenções originais de cada padrão.
-
Design Patterns PHP — Documentação em português Guia https://designpatternsphp.readthedocs.io/pt_BR/latest/
Todos os 23 padrões GoF implementados em PHP moderno, com código pronto para rodar e testes PHPUnit. Ótimo repositório de referência para os próximos artigos.
-
MARTIN, R.C. — Clean Code. Cap. 3: Funções e construtores limpos. 2008 Livro https://www.oreilly.com/library/view/clean-code-a/9780136083238/
O argumento contra construtores com muitos parâmetros — a base teórica para o padrão Builder. Martin defende que funções/construtores ideais têm zero a três parâmetros.
Exercícios
Exercício 1
Implemente um Singleton para gerenciar configurações do sistema (Config). Ele deve carregar um arquivo config.php que retorna um array na primeira chamada e expor métodos get(string $chave, mixed $padrao = null): mixed e set(string $chave, mixed $valor): void. Verifique com === que duas chamadas a Config::getInstance() retornam o mesmo objeto.
Ver resposta
✓ Resposta: A última linha é o que o === do enunciado prova: $b vê o que $a escreveu porque são o mesmo objeto. Vale saber o outro lado: esse estado global compartilhado é justamente o que torna Singleton difícil de testar — em código novo, injeção de dependência costuma ser a escolha melhor.
<?php
declare(strict_types=1);
final class Config
{
private static ?self $instancia = null;
private array $dados = [];
// Os três bloqueios que fazem o Singleton valer: sem `new`, sem `clone`
// e sem `unserialize` criando uma segunda instância pelas costas.
private function __construct()
{
$caminho = __DIR__ . '/config.php';
if (!is_file($caminho)) {
throw new RuntimeException("config.php não encontrado em {$caminho}");
}
$this->dados = require $caminho; // o arquivo retorna um array
}
private function __clone() {}
public function __wakeup(): void
{
throw new LogicException('Config não pode ser desserializado.');
}
public static function getInstance(): self
{
// O carregamento do arquivo acontece uma vez só, na primeira chamada.
return self::$instancia ??= new self();
}
/** Aceita chave com ponto: get('db.host') */
public function get(string $chave, mixed $padrao = null): mixed
{
$atual = $this->dados;
foreach (explode('.', $chave) as $parte) {
if (!is_array($atual) || !array_key_exists($parte, $atual)) {
return $padrao;
}
$atual = $atual[$parte];
}
return $atual;
}
public function set(string $chave, mixed $valor): void
{
$partes = explode('.', $chave);
$atual = &$this->dados;
foreach ($partes as $parte) {
if (!isset($atual[$parte]) || !is_array($atual[$parte])) {
$atual[$parte] = [];
}
$atual = &$atual[$parte];
}
$atual = $valor;
}
}
// config.php seria:
// <?php return ['app' => ['nome' => 'Loja'], 'db' => ['host' => '127.0.0.1']];
$a = Config::getInstance();
$b = Config::getInstance();
var_dump($a === $b); // bool(true) — mesma instância
echo $a->get('db.host'), PHP_EOL; // 127.0.0.1
echo $a->get('db.porta', 3306), PHP_EOL; // 3306 (padrão)
$a->set('db.porta', 5432);
echo $b->get('db.porta'), PHP_EOL; // 5432 — b enxerga o que a escreveu
Exercício 2
Crie uma Factory para parsers de arquivo: interface ParserInterface com método parse(string $conteudo): array. Implemente JsonParser, CsvParser e XmlParser. A ParserFactory::criar(string $extensao) retorna o parser correto baseado na extensão (.json, .csv, .xml). Lança ParserNaoSuportadoException para extensões desconhecidas.
Ver resposta
✓ Resposta: Quem chama a factory nunca escreve new JsonParser: recebe ParserInterface e pronto. Formato novo é uma linha no mapa — e o throw como expressão no ?? evita o if de sempre.
<?php
declare(strict_types=1);
interface ParserInterface
{
/** @return array<int|string, mixed> */
public function parse(string $conteudo): array;
}
final class ParserNaoSuportadoException extends InvalidArgumentException
{
public function __construct(public readonly string $extensao)
{
parent::__construct("Não há parser para a extensão '{$extensao}'.");
}
}
final class JsonParser implements ParserInterface
{
public function parse(string $conteudo): array
{
// JSON_THROW_ON_ERROR troca o retorno null silencioso por exceção.
return json_decode($conteudo, true, 512, JSON_THROW_ON_ERROR);
}
}
final class CsvParser implements ParserInterface
{
public function __construct(private readonly string $separador = ',') {}
public function parse(string $conteudo): array
{
$linhas = preg_split('/\r\n|\r|\n/', trim($conteudo));
$cabecalho = str_getcsv(array_shift($linhas), $this->separador);
return array_map(
fn(string $linha): array => array_combine(
$cabecalho,
str_getcsv($linha, $this->separador),
),
array_filter($linhas, static fn(string $l): bool => trim($l) !== ''),
);
}
}
final class XmlParser implements ParserInterface
{
public function parse(string $conteudo): array
{
$anterior = libxml_use_internal_errors(true);
// LIBXML_NONET fecha a porta de XXE — XML de terceiro não deve
// conseguir buscar entidade externa pela rede.
$xml = simplexml_load_string($conteudo, null, LIBXML_NONET | LIBXML_NOCDATA);
libxml_use_internal_errors($anterior);
if ($xml === false) {
throw new RuntimeException('XML inválido.');
}
return json_decode(json_encode($xml), true);
}
}
final class ParserFactory
{
private const MAPA = [
'json' => JsonParser::class,
'csv' => CsvParser::class,
'xml' => XmlParser::class,
];
public static function criar(string $extensao): ParserInterface
{
// Normaliza ".JSON", "json" e ".json" para a mesma chave.
$chave = strtolower(ltrim($extensao, '.'));
$classe = self::MAPA[$chave] ?? throw new ParserNaoSuportadoException($chave);
return new $classe();
}
public static function paraArquivo(string $caminho): ParserInterface
{
return self::criar(pathinfo($caminho, PATHINFO_EXTENSION));
}
}
// Uso:
$parser = ParserFactory::criar('.json');
print_r($parser->parse('{"nome":"Onix","preco":89900}'));
$csv = ParserFactory::criar('csv');
print_r($csv->parse("nome,preco\nOnix,89900\nHB20,87500"));
try {
ParserFactory::criar('.docx');
} catch (ParserNaoSuportadoException $e) {
echo $e->getMessage(), PHP_EOL; // Não há parser para a extensão 'docx'.
}
Exercício 3
Construa um Builder para gerar queries SQL SELECT: QueryBuilder com métodos from(string $tabela), select(string ...$colunas), where(string $condicao), orderBy(string $coluna, string $direcao = 'ASC'), limit(int $n) e build(): string. O build() retorna a string SQL completa e válida.
Ver resposta
✓ Resposta: Repare que where() aqui recebe a condição pronta — é o que o enunciado pede, e serve para aprender a montagem. Em código de produção, valor vindo do usuário nunca entra por concatenação: vira placeholder, como no QueryBuilder do artigo sobre PDO.
<?php
declare(strict_types=1);
final class QueryBuilder
{
private string $tabela = '';
private array $colunas = ['*'];
private array $condicoes = [];
private array $ordenacao = [];
private ?int $limite = null;
public function from(string $tabela): static
{
$this->tabela = $tabela;
return $this;
}
public function select(string ...$colunas): static
{
if ($colunas !== []) {
$this->colunas = $colunas;
}
return $this;
}
public function where(string $condicao): static
{
$this->condicoes[] = $condicao;
return $this;
}
public function orderBy(string $coluna, string $direcao = 'ASC'): static
{
$direcao = strtoupper($direcao);
if (!in_array($direcao, ['ASC', 'DESC'], true)) {
throw new InvalidArgumentException("Direção inválida: {$direcao}");
}
$this->ordenacao[] = "{$coluna} {$direcao}";
return $this;
}
public function limit(int $n): static
{
if ($n < 1) {
throw new InvalidArgumentException('LIMIT tem de ser >= 1.');
}
$this->limite = $n;
return $this;
}
public function build(): string
{
if ($this->tabela === '') {
throw new LogicException('Defina a tabela com from() antes de build().');
}
// A ordem das cláusulas é fixa em SQL — o builder garante isso mesmo
// que os métodos tenham sido chamados fora de ordem.
$sql = 'SELECT ' . implode(', ', $this->colunas)
. ' FROM ' . $this->tabela;
if ($this->condicoes !== []) {
$sql .= ' WHERE ' . implode(' AND ', $this->condicoes);
}
if ($this->ordenacao !== []) {
$sql .= ' ORDER BY ' . implode(', ', $this->ordenacao);
}
if ($this->limite !== null) {
$sql .= ' LIMIT ' . $this->limite;
}
return $sql;
}
}
// O `return $this` de cada método é o que permite a corrente:
echo (new QueryBuilder())
->select('id', 'nome', 'preco')
->from('produtos')
->where('ativo = 1')
->where('preco > 5000')
->orderBy('preco', 'DESC')
->limit(10)
->build();
// SELECT id, nome, preco FROM produtos
// WHERE ativo = 1 AND preco > 5000 ORDER BY preco DESC LIMIT 10
Exercício 4
Crie um Director para o QueryBuilder do exercício 3 com dois métodos: queryPaginada(string $tabela, int $pagina, int $porPagina): string e queryRecentes(string $tabela, string $campoData, int $limite): string.
Ver resposta
✓ Resposta: A divisão de trabalho do padrão: o Builder sabe como montar cada pedaço, o Director sabe quais pedaços formam uma receita conhecida. O erro de página zero morre no Director, não vira SQL com OFFSET negativo.
<?php
declare(strict_types=1);
/**
* O Director guarda as receitas de montagem que se repetem.
* Quem chama não precisa lembrar que paginação é OFFSET = (página-1) * porPágina.
*/
final class QueryDirector
{
public function queryPaginada(string $tabela, int $pagina, int $porPagina): string
{
if ($pagina < 1) {
throw new InvalidArgumentException('A página começa em 1.');
}
$offset = ($pagina - 1) * $porPagina;
$sql = (new QueryBuilder())
->from($tabela)
->orderBy('id')
->limit($porPagina)
->build();
// OFFSET não está no builder do exercício 3 — sai daqui.
return $offset > 0 ? "{$sql} OFFSET {$offset}" : $sql;
}
public function queryRecentes(string $tabela, string $campoData, int $limite): string
{
return (new QueryBuilder())
->from($tabela)
->orderBy($campoData, 'DESC')
->limit($limite)
->build();
}
}
$director = new QueryDirector();
echo $director->queryPaginada('produtos', pagina: 3, porPagina: 20), PHP_EOL;
// SELECT * FROM produtos ORDER BY id ASC LIMIT 20 OFFSET 40
echo $director->queryRecentes('pedidos', 'criado_em', 5), PHP_EOL;
// SELECT * FROM pedidos ORDER BY criado_em DESC LIMIT 5
Exercício 5
Desafio: combine os três padrões num mini-sistema de relatórios. A RelatorioFactory cria implementações de RelatorioInterface (PDF, CSV, HTML). Cada relatório é configurado por um RelatorioBuilder com filtros de data, colunas visíveis e ordenação. O ConfiguracaoRelatorio é um Singleton que guarda os formatos disponíveis e os limites de linhas por relatório.
Ver resposta
✓ Resposta: Cada padrão responde por uma pergunta diferente e não invade a do vizinho: a Factory decide qual classe nasce, o Builder decide com que configuração, e o Singleton guarda o que é global de verdade. Quando um padrão começa a responder a pergunta do outro, é sinal de que o desenho azedou.
<?php
declare(strict_types=1);
// ---------- SINGLETON: limites e formatos, um só para a aplicação ----------
final class ConfiguracaoRelatorio
{
private static ?self $instancia = null;
private array $formatos = ['pdf', 'csv', 'html'];
private int $limiteLinhas = 5000;
private function __construct() {}
private function __clone() {}
public static function getInstance(): self
{
return self::$instancia ??= new self();
}
public function formatos(): array { return $this->formatos; }
public function limiteLinhas(): int { return $this->limiteLinhas; }
public function definirLimite(int $linhas): void
{
$this->limiteLinhas = max(1, $linhas);
}
}
// ---------- STRATEGY/FACTORY: um relatório por formato --------------------
interface RelatorioInterface
{
public function render(array $linhas, array $colunas): string;
public function extensao(): string;
}
final class RelatorioCsv implements RelatorioInterface
{
public function render(array $linhas, array $colunas): string
{
$saida = fopen('php://temp', 'r+');
fputcsv($saida, $colunas);
foreach ($linhas as $linha) {
// Só as colunas visíveis, na ordem pedida.
fputcsv($saida, array_map(fn($c) => $linha[$c] ?? '', $colunas));
}
rewind($saida);
$conteudo = stream_get_contents($saida);
fclose($saida);
return $conteudo;
}
public function extensao(): string { return 'csv'; }
}
final class RelatorioHtml implements RelatorioInterface
{
public function render(array $linhas, array $colunas): string
{
$html = "<table>\n<thead><tr>";
foreach ($colunas as $coluna) {
// htmlspecialchars em TODO dado que vai para a página.
$html .= '<th>' . htmlspecialchars((string) $coluna, ENT_QUOTES) . '</th>';
}
$html .= "</tr></thead>\n<tbody>\n";
foreach ($linhas as $linha) {
$html .= '<tr>';
foreach ($colunas as $coluna) {
$html .= '<td>' . htmlspecialchars((string) ($linha[$coluna] ?? ''), ENT_QUOTES) . '</td>';
}
$html .= "</tr>\n";
}
return $html . "</tbody>\n</table>";
}
public function extensao(): string { return 'html'; }
}
final class RelatorioPdf implements RelatorioInterface
{
// Sem dependência externa aqui: gera o corpo que uma lib (Dompdf, mPDF)
// receberia. O padrão é o mesmo, muda só o motor de saída.
public function render(array $linhas, array $colunas): string
{
$corpo = (new RelatorioHtml())->render($linhas, $colunas);
return "%PDF-1.4 (simulado)\n" . strip_tags($corpo, '<table><tr><td><th>');
}
public function extensao(): string { return 'pdf'; }
}
final class RelatorioFactory
{
public static function criar(string $formato): RelatorioInterface
{
$formato = strtolower($formato);
if (!in_array($formato, ConfiguracaoRelatorio::getInstance()->formatos(), true)) {
throw new InvalidArgumentException("Formato não habilitado: {$formato}");
}
return match ($formato) {
'csv' => new RelatorioCsv(),
'html' => new RelatorioHtml(),
'pdf' => new RelatorioPdf(),
};
}
}
// ---------- BUILDER: filtros, colunas e ordenação -------------------------
final class RelatorioBuilder
{
private ?DateTimeImmutable $de = null;
private ?DateTimeImmutable $ate = null;
private array $colunas = [];
private ?string $ordenarPor = null;
private string $direcao = 'ASC';
public function periodo(DateTimeImmutable $de, DateTimeImmutable $ate): static
{
if ($de > $ate) {
throw new InvalidArgumentException('Início posterior ao fim do período.');
}
$this->de = $de;
$this->ate = $ate;
return $this;
}
public function colunas(string ...$colunas): static
{
$this->colunas = $colunas;
return $this;
}
public function ordenarPor(string $coluna, string $direcao = 'ASC'): static
{
$this->ordenarPor = $coluna;
$this->direcao = strtoupper($direcao) === 'DESC' ? 'DESC' : 'ASC';
return $this;
}
public function gerar(array $dados, RelatorioInterface $relatorio): string
{
$linhas = $dados;
if ($this->de !== null) {
$linhas = array_filter($linhas, function (array $l): bool {
$data = new DateTimeImmutable($l['data']);
return $data >= $this->de && $data <= $this->ate;
});
}
if ($this->ordenarPor !== null) {
$col = $this->ordenarPor;
$dir = $this->direcao === 'DESC' ? -1 : 1;
usort($linhas, fn(array $a, array $b): int => ($a[$col] <=> $b[$col]) * $dir);
}
// O Singleton é a última palavra sobre o tamanho do relatório.
$linhas = array_slice($linhas, 0, ConfiguracaoRelatorio::getInstance()->limiteLinhas());
$colunas = $this->colunas ?: array_keys($linhas[0] ?? []);
return $relatorio->render($linhas, $colunas);
}
}
// ---------- Juntando tudo -------------------------------------------------
$dados = [
['data' => '2026-03-01', 'produto' => 'Onix', 'total' => 89900],
['data' => '2026-03-14', 'produto' => 'HB20', 'total' => 87500],
['data' => '2026-04-02', 'produto' => 'Kwid', 'total' => 71200],
];
echo (new RelatorioBuilder())
->periodo(new DateTimeImmutable('2026-03-01'), new DateTimeImmutable('2026-03-31'))
->colunas('data', 'produto', 'total')
->ordenarPor('total', 'DESC')
->gerar($dados, RelatorioFactory::criar('csv'));
// data,produto,total
// 2026-03-01,Onix,89900
// 2026-03-14,HB20,87500