Structural Engineering and Design
Designing a Flexible Email Campaign System with the Builder Pattern in Laravel
Table of Contents
O marketing moderno exige campanhas de email que se adaptam rapidamente aos segmentos de audiência, testes A/B e conteúdo dinâmico. Uma implementação rígida e monolítica rapidamente se torna um pesadelo de manutenção à medida que os requisitos evoluem. Laravel, com sua sintaxe expressiva e ecossistema robusto, fornece uma base ideal, mas a verdadeira chave para a flexibilidade de longo prazo reside na escolha do padrão de design certo. O Padrão do Construtor se destaca como uma solução poderosa para construir objetos complexos de email passo a passo, permitindo que você troque componentes sem reescrever a lógica do núcleo.
Neste guia abrangente, você aprenderá a projetar um sistema de campanha de email flexível em Laravel usando o Padrão do Construtor. Nós vamos quebrar a teoria, percorrer uma implementação detalhada e explorar a integração com os sistemas de discussão e fila de Laravel. No final, você terá uma abordagem pronta para a produção de gerar qualquer variante de campanha de email – de mensagens transacionais de texto simples para promoções HTML ricas – com duplicação de código mínima.
Qual é o padrão do construtor?
O Padrão do Construtor é um padrão de design criacional que separa a construção de um objeto complexo da sua representação final. Em vez de criar um objeto através de um construtor maciço ou de um conjunto de métodos de fábrica, você delega o processo de construção para um diretor dedicado e classes de construtor. O diretor orquestra os passos, enquanto cada construtor sabe como montar os componentes para uma variante específica.
Este padrão brilha quando um objeto requer muitas partes opcionais, tem várias etapas de configuração, ou quando você precisa produzir diferentes representações de objetos semelhantes. Para campanhas de e-mail, o “objeto” é uma mensagem de e-mail – completa com assunto, corpo, anexos, destinatários, cabeçalhos e metadados. Cada tipo de campanha (promocional, transacional, desencadeada por eventos) pode compartilhar uma base comum, mas diferir em layout, informações de remetente ou blocos de conteúdo.
Como Difere do padrão de fábrica
Enquanto o Padrão de Fábrica se concentra na criação de objetos em uma única chamada, o Padrão do Construtor permite um processo de construção controlado e de pedaços. As fábricas são ideais quando a lógica de criação é simples; os construtores são melhores quando você precisa controlar a ordem e seleção de peças. Em sistemas de e-mail, você muitas vezes precisa adicionar anexos condicionalmente, variar o modelo do corpo, ou definir prioridades de entrega diferentes. O Padrão do Construtor lhe dá esse controle granular sem inchar uma única classe de fábrica.
Configurando seu ambiente Laravel
Antes de mergulhar em código, certifique-se de ter uma aplicação Laravel (versão 9 ou posterior) com a configuração padrão de email. Assumiremos que você tenha as tabelas de banco de dados necessárias para campanhas, assinantes e modelos – mas para este artigo, focamos na lógica do construtor em si. Você pode seguir com uma nova instalação Laravel usando o Compositor:
composer create-project laravel/laravel email-campaign-builder
Em seguida, configure o seu driver de e-mail em (por exemplo, ]] para testes). Nós também usaremos a fachada de Laravel] e as classes incorporadas mais tarde, mas nosso construtor permanecerá independente deles para manter a separação limpa.
Componentes Principais do Padrão do Construtor
Iremos implementar quatro componentes principais:
- Produto – O objeto de e-mail final (pode ser um objeto de valor simples , um objeto de valor , ou uma extensão do objeto de Laravel ]).
- Interface do Construtor – Declara métodos para cada parte do e-mail: assunto, corpo, destinatários, anexos, cabeçalhos, etc.
- Construtores de Concreto – Cada um implementa a interface para um tipo de email específico (Promocional, Transacional, Bem-vindo).
- Director – Orchestra as etapas de construção em uma ordem definida, muitas vezes usando o mesmo construtor para produzir vários e-mails de um projeto.
Passo 1: Defina o produto da mensagem de e-mail
Criaremos um objeto de valor simples para manter todos os dados de e-mail. Isso mantém nosso código construtor limpo e testável.
<?php
namespace App\Values;
class EmailMessage
{
public string $subject;
public string $body;
public string $mimeType = 'text/html'; // or text/plain
public array $recipients = [];
public array $ccRecipients = [];
public array $bccRecipients = [];
public array $attachments = [];
public array $headers = [];
public ?string $fromAddress = null;
public ?string $fromName = null;
public function toArray(): array
{
return [
'subject' => $this->subject,
'body' => $this->body,
'mimeType' => $this->mimeType,
'recipients' => $this->recipients,
'cc' => $this->ccRecipients,
'bcc' => $this->bccRecipients,
'attachments' => $this->attachments,
'headers' => $this->headers,
'from' => ['address' => $this->fromAddress, 'name' => $this->fromName],
];
}
}
Passo 2: Interface do Construtor
A interface define o contrato para a construção de qualquer variante de email.
<?php
namespace App\Builders\Contracts;
use App\Values\EmailMessage;
interface EmailBuilderContract
{
public function setSubject(string $subject): self;
public function setBody(string $body, string $mimeType = 'text/html'): self;
public function addRecipient(string $email, ?string $name = null): self;
public function addCc(string $email, ?string $name = null): self;
public function addBcc(string $email, ?string $name = null): self;
public function addAttachment(string $filePath, ?string $name = null): self;
public function addHeader(string $key, string $value): self;
public function setFrom(string $address, ?string $name = null): self;
public function getEmail(): EmailMessage;
public function reset(): void;
}
Etapa 3: Construtor de concreto para e-mails promocionais
Vamos implementar um construtor que personalize o email para promoções – adicionar pixels de rastreamento, links de compartilhamento social e um rodapé padrão de cancelamento de assinatura.
<?php
namespace App\Builders;
use App\Builders\Contracts\EmailBuilderContract;
use App\Values\EmailMessage;
class PromotionalEmailBuilder implements EmailBuilderContract
{
private EmailMessage $email;
public function __construct()
{
$this->reset();
}
public function setSubject(string $subject): self
{
$this->email->subject = '[Promo] ' . $subject;
return $this;
}
public function setBody(string $body, string $mimeType = 'text/html'): self
{
// Wrap body with promotional header/footer
$this->email->body = $this->wrapBody($body);
$this->email->mimeType = $mimeType;
return $this;
}
private function wrapBody(string $body): string
{
return "<div style=\"background:#f5f5f5; padding:20px;\">
<div style=\"max-width:600px; margin:auto;\">
$body
<hr>
<p style=\"font-size:12px; color:#888;\">
You received this because you opted in.
<a href=\"{{unsubscribe_url}}\">Unsubscribe</a>
</p>
</div>
</div>";
}
public function addRecipient(string $email, ?string $name = null): self
{
$this->email->recipients[] = compact('email', 'name');
return $this;
}
public function addCc(string $email, ?string $name = null): self
{
$this->email->ccRecipients[] = compact('email', 'name');
return $this;
}
public function addBcc(string $email, ?string $name = null): self
{
$this->email->bccRecipients[] = compact('email', 'name');
return $this;
}
public function addAttachment(string $filePath, ?string $name = null): self
{
$this->email->attachments[] = ['path' => $filePath, 'name' => $name];
return $this;
}
public function addHeader(string $key, string $value): self
{
$this->email->headers[$key] = $value;
return $this;
}
public function setFrom(string $address, ?string $name = null): self
{
$this->email->fromAddress = $address;
$this->email->fromName = $name;
return $this;
}
public function getEmail(): EmailMessage
{
$built = clone $this->email;
$this->reset();
return $built;
}
public function reset(): void
{
$this->email = new EmailMessage();
}
}
Passo 4: Exemplo do Construtor Transacional
Um email transacional (por exemplo, confirmação de ordem) precisa de um papel diferente – branding mínimo, cabeçalhos de prioridade, e nenhum rodapé de cancelamento.
<?php
namespace App\Builders;
class TransactionalEmailBuilder implements EmailBuilderContract
{
private EmailMessage $email;
// ... same structure, but setSubject does not prepend prefix
// and wrapBody() uses a simple layout with order details
// getEmail() resets the builder
}
O Diretor: Orquestrando a Construção
O diretor toma uma instância construtora e chama seus passos em uma ordem específica. É aqui que você pode definir sequências padrão, como “construir um email de campanha para um determinado assinante.”
<?php
namespace App\Builders;
use App\Builders\Contracts\EmailBuilderContract;
use App\Models\User;
class CampaignDirector
{
public function __construct(private EmailBuilderContract $builder) {}
public function buildPromotionalCampaign(User $user, string $subject, string $body): EmailMessage
{
return $this->builder
->setFrom('[email protected]', 'Marketing Team')
->setSubject($subject)
->addRecipient($user->email, $user->name)
->addHeader('X-Campaign-Id', $campaignId)
->setBody($body)
->getEmail();
}
public function buildTransactionalOrderConfirmation(Order $order): EmailMessage
{
// Switch builder if needed, or use a different director method
// (In practice, you'd instantiate a TransactionalEmailBuilder)
$this->builder = new TransactionalEmailBuilder();
$body = view('emails.order-confirmation', compact('order'))->render();
return $this->builder
->setFrom('[email protected]', 'Order System')
->setSubject('Order Confirmation #' . $order->id)
->addRecipient($order->user->email, $order->user->name)
->setBody($body)
->addHeader('X-Transaction-Id', $order->transaction_id)
->addAttachment(storage_path('invoices/' . $order->invoice_file))
->getEmail();
}
}
Usando um diretor mantém a lógica de construção centralizada. Se você mais tarde precisa adicionar um método , você apenas estende o diretor sem tocar nos construtores.
Integrando com o Correio Laravel
Uma vez que você tem um objeto de valor , você precisa enviá-lo. Crie um Mailable personalizado que aceita o ] e o renderiza usando o sistema de e-mail integrado de Laravel.
<?php
namespace App\Mail;
use App\Values\EmailMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CampaignMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public EmailMessage $emailMessage) {}
public function envelope(): Envelope
{
return new Envelope(
from: $this->emailMessage->fromAddress
? new Address($this->emailMessage->fromAddress, $this->emailMessage->fromName)
: null,
subject: $this->emailMessage->subject,
cc: $this->emailMessage->ccRecipients,
bcc: $this->emailMessage->bccRecipients,
headers: $this->emailMessage->headers,
);
}
public function content(): Content
{
return new Content(
htmlString: $this->emailMessage->body,
);
}
public function attachments(): array
{
return array_map(function ($attach) {
return Attachment::fromPath($attach['path'])
->as($attach['name'] ?? null);
}, $this->emailMessage->attachments);
}
}
Agora você pode enviar e-mails de qualquer controlador ou trabalho usando o construtor:
use App\Builders\PromotionalEmailBuilder;
use App\Builders\CampaignDirector;
use App\Mail\CampaignMail;
use Illuminate\Support\Facades\Mail;
$builder = new PromotionalEmailBuilder();
$director = new CampaignDirector($builder);
$emailMessage = $director->buildPromotionalCampaign($user, 'Summer Sale', $htmlContent);
Mail::to($user->email)->send(new CampaignMail($emailMessage));
Aproveitando a fila para escalabilidade
Os sistemas de campanha de email devem lidar com milhares de destinatários assincronicamente. O sistema de filas de Laravel é uma combinação perfeita. Enrole a lógica de envio em uma tarefa em fila que usa o construtor para cada destinatário.
<?php
namespace App\Jobs;
use App\Builders\Contracts\EmailBuilderContract;
use App\Builders\CampaignDirector;
use App\Mail\CampaignMail;
use App\Models\Subscriber;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendCampaignEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private Subscriber $subscriber,
private string $subject,
private string $body,
private string $builderClass // class-string
) {}
public function handle(): void
{
$builder = app($this->builderClass);
$director = new CampaignDirector($builder);
$emailMessage = $director->buildPromotionalCampaign(
$this->subscriber->user,
$this->subject,
$this->body
);
Mail::to($this->subscriber->email)
->send(new CampaignMail($emailMessage));
}
}
Envie o trabalho para cada assinante em um loop (ou melhor, use ] para gerenciar envios bem sucedidos/falhados).
Adicionando modelos dinâmicos com lâmina
A codificação de HTML em construtores não é ideal. Em vez disso, passe as visualizações renderizadas do Blade como o corpo. Seu construtor pode aceitar um nome de visualização e uma matriz de dados, então chame dentro . Isto mantém seus modelos separados e fáceis de editar para os designers.
public function setBody(string $viewName, array $data = [], string $mimeType = 'text/html'): self
{
$rendered = view($viewName, $data)->render();
$this->email->body = $this->wrapBody($rendered);
$this->email->mimeType = $mimeType;
return $this;
}
Agora você pode chamar .
Benefícios em Campanhas do Mundo Real
- Variedade sem duplicação – Diferentes tipos de campanha compartilham a mesma interface; construtores encapsulam as diferenças.
- Testabilidade – Você pode testar cada construtor por unidade, recuperando o e afirmando suas propriedades.
- Teste A/B mais fácil – Troca de construtores por grupo variante. O processo de construção do diretor permanece inalterado.
- Auditar trilhas – Adicionar login dentro do construtor para gravar cada passo para análise posterior.
- Integração com serviços externos – Use o construtor para montar cargas úteis para serviços como Mailgun, SendGrid ou SparkPost.
Melhores práticas e armadilhas comuns
Manter os construtores sem Estado onde possível
O método garante que um construtor possa ser reutilizado. Se você esquecer de chamar , a mesma instância do construtor pode vazar estado entre diferentes campanhas. Em nosso exemplo, chamadas automaticamente – um padrão seguro.
Não super-enginee para e-mails simples
Se a sua aplicação enviar apenas um tipo de e- mail, o Padrão do Construtor poderá ser exagerado. Brilha quando tiver pelo menos três tipos de e- mail distintos com componentes variáveis.
Usar a injeção de dependência para os construtores
Registre seus construtores no contêiner de serviço para que você possa injetar dependências (como serviços de registro ou rastreamento) neles facilmente.
Cuidado com o número de beneficiários
O construtor não deve coletar milhares de destinatários em um único – que carregaria gigabytes na memória. Em vez disso, criar um e-mail por destinatário, ou usar APIs em lote (Enviargun ]). O diretor pode fazer um loop sobre um pedaço de assinantes.
Outras melhorias
Considere adicionar um – um modelo Eloquent que armazena a classe de construtor, modelo e parâmetros padrão. Então, um trabalho de agendamento lê projetos e usa o diretor para criar e-mails de fila do & para todos os assinantes ativos.
Você também pode introduzir um que aplica regras globais (como sempre adicionar um link de cancelamento de assinatura) antes de delegar para o construtor específico. Isto adiciona outra camada de separação.
Recursos externos
- Padrão do construtor – Guru de refatorização (excelente explicação visual)
- Documentação de Correios Laravel (para referência de classe de envio de correio)
- Laravel Filas (para escalar envio de e-mail)
Conclusão
Desenhar um sistema de campanha de email flexível não requer uma estrutura maciça. Com o Padrão do Construtor em Laravel, você ganha controle preciso sobre a construção de email, tornando seu banco de código adaptável às mudanças nas necessidades de marketing. Ao separar o que (construtor) do que (diretor), você permite que as equipes adicionem novos tipos de campanha sem medo de quebrar os existentes. Combine isso com a infraestrutura de email e fila de Laravel, e você tem uma solução escalável, testável e mantetível que servirá suas campanhas por anos.