Table of Contents
Modern marketing demands email kampanins that adaptat rapidly tos audience segments, A / B tests, and dynamic content. A rigid, monolithic implementation quickline becomes a acquidance nightmare as requirements evolvne. Laravel, with its expressive syntax androbutt ecosystem, provides an ideal foundation, but thee real key tlo long-term explity ies in choosing thee right design ecoustic. The Builder faclan stand out a powerful solouttin for constructing complex emm emm emps steb, alint theg you contribuents.
In this conclussive guide, you will learn how to architect a explixble email campaign system in Laravel using thee Builder Pattern. We will breaks down thee theory, walk thrugh a detaid implementation, and explaire integration with Laravel 's mailing ande queue systems. By the end, you' ll have a production- ready approposaph tich generating any email companign variant - from belt -text transactionals end rich HTL promotions - mitraid-micrope duplicatin.
Co to jest Builder Pattern?
Thee Builder Pattern is a creational design plant that separates thee construction of a complex object from it final reprezentatywna. Instad of creating an object via a massive constructor or a set of factory methods, you delegte the building process to a dedicated director and builder classes. The director orchestrates thee steps, while each builder knows how to asmemble thee concreents for a specific variant.
This plant shine when un object requires many optional parts, has multiple configuration steps, or when you need two produce differents of similar objects. For email kampanigns, the acquent quote; object conquict quent; is an email message - complete with sub, body, attribuments, recipients, headers, ande metadata. Each accign type (promotionation, transactional, event- triggered) may share a mean base but difyan layout, sender o, or content blocks.
How It Differs from the Factory Pattern
Kiedy Factory Plant Focuses one creation objects in a single call, thee Builder Pattern allows for a controlled, piecomed l construction process. Factories are ideal whether thee creation logic is simple; builders are better whein you need to control thee order andd selection of parts. In email systems, you often need te conditionally add attribuilments, vary the body template, or set differentities. Thee Builder Amenn gives u thattent controull controut bloating a single factore class.
Setting Up Your Laravel Environment
Before diving into code, ensure you have a Laravel application (version 9 or later) wigh the default mail configuation. We 'll assume you have thee necessary database tables for kampanins, subscribers, and templates - but for this article, we focus on thee builder logic itself. You can follow along with a fresh Laravel install using Compose:
composer create-project laravel/laravel email-campaign-builder
Next, configure e your mail disr in providen1; Xi1; FLT: 1 XI3; XI3; (np., XI1; XI1; FLT: 2 XI3; XI3; FOR testing). We will also use Laravel 's bedis1; XI1; FLT: 3 XI3; XI3; FLT: 3 XI3; Facade ande the built- in bedis1; XI1; FLT: 4 XI3; classes later, builder will pertiin exient of them tim tán cleain separation.
Core Components of thee Builder Pattern
We will implement four core contribuents:
- (Dz.U. L 311 z 15.11.2014, s. 1).
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Builder Interface Xi1; Xi1; FLT: 1 Xi3; Xi3; - Declares methods for each part of the email: subett, body, recipiens, attachments, headers, etc.
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Concrete Builders Xi1; Xi1; FLT: 1 Xi3; Xi3; - Each implements the interface for a specific email type (Promotional, Transactional, Welcome serie).
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Director Xi1; Xi1; FLT: 1 Xi3; Xi3; - Orchestrates the building steps in a definid order, often using thee same builder to produce multiple emails from a blueprint.
Step 1: Definite thee Email Message Product
This keeps our builder code clean andtestable.
<?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],
];
}
}
Step 2: Builder Interface
Te interface definiują te umowy for building any email variant.
<?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;
}
Step 3: Concrete Builder for Promotional Emails
Let 's implement a builder that tailors thee email for promotions - adding tracking pixels, social share links, and a standard unsubscribe footir.
<?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();
}
}
Step 4: Transactional Builder Example
Transactional email (np., order confirmation) needs a different wrapper - minimal branding, priority headers, ando no unsubscribe footer.
<?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
}
Thee Director: Orchestrating thee Build
Thee director takes a builder instance ands calls it steps in a specific order. This is where you can define standard sequeres, such as contribution quality; build a campaign email for a given subscriber. contribution quality;
<?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();
}
}
Using a director keeps construction logic centralized. If you later need to add a eng1; Ig1; FLT: 13 construction logic centoryzed. If you later to add a engine; FLT: 13 constructione3; Igd, you just extend the director with out touching the builders.
Integrating wigh Laravel Mail
Once you have a message 1; Employ1; FLT: 14 message3; Employ3; value object, you need too send it. Create a create a custem Mailable that accepts the message1; Employ1; FLT: 15 message3; employ3; and renders it using Laravel 's built- in mail system.
<?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);
}
}
Now you can send emails from any controller or joba using the builder:
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));
Leveraging the Queue for Scalability
Email campaign systems mutt handle tysięczne i s of recipients asynchronously. Laravel 's queue systeme is a perfect match. Wrap the sending logic in a queued joba that uses the builder for each recipient.
<?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));
}
}
Dispatch the jobe for each subscriber in a loop (or better, use bett1; ett1; FLT: 19 bett3; ett3; to manage successful / faileed sends).
Adding Dynamic Templates with Blade
Hardcoding HTML in builders is not ideal. Instad, pass rendered Blade views as the body. You r builder can accort a view name andd data array, then call indir 1; FLT: 20 message 3; FLT: 20 message 3; inside message 1; FLT: 21 message 3; endirec3; This keeps your templates separate andd esy for designers to edit.
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;
}
Nowa You can call (*) 1; Siódma; FLT: 23 Siódma; Siódma;
Korzyści z kampanii na świecie
- Varietynez duplication 1; Vari1; FLT: 1 Vario1; FLT: 1 Various 3; FLT: 0 Various 3; Various Without Duplication 1; Various 1; FLT: 1 Various 3; FLT: Various 3; - Different campaign types share thee same interface; builders encapsulate thee differences.
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Xi3; Xi1; FLT: 1 Xi3; Xi3; - You can unit- tect each builder byreteving the Xion1; Xion1; FLT: 24 Xion3; Xion3; and asserting its performanties.
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Easier A / B Testing Xi1; Xi1; FLT: 1 Xi3; Xi3; - Swap builders per variant group. The director 's construction process pozostaje niezmienny.
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Audit trails Xi1; Xi1; FLT: 1 Xi3; Xi3; - Add logging inside the builder to Xiond every step for later analysis.
- Xi1; Xi1; FLT: 0 Xi3; Xi3; Integration with external services is between 1; Xi1; FLT: 1 Xi3; Xi3; - Usie te builder to assemble payloads for services like Mailgun, SendGrid, or SparkPoct.
Bett Practices andCommon Pitfalls
Keep Builders Stateless Where Possible
Thee ensures a builder can be reused. If you forget to call contaminant1; FLT: 26 contaminant3; FLT: 26 containt3;, thee same builder instance may leak state between different campaigns. In our example, entainment 1; FLT: 27 containt3; fLT: 27 containt3; calls 1; FLT: 28 containtac 3; exat3; automatically - a safe contagent.
Don 't Over- Engineer for Simple Emails
If your application sends only ony kind of email, thee Builder Pattern might be overkill. It shines when you have at leaast three distint email type with varying contribuents.
Use Dependency Injection for Builders
Register your builders in the service container so you can inject dependencies (like logging or tracking services) into them esily.
Mind thee Number of Recipiens
Te builder nie powinny zbierać tysięcznych i of recipients in a single eng1; ingel1; FLT: 29 edis3; ing3; - that would load gigabajtes into memory. Instad, create one email per recipient, or use batch API (Mailgun 's engine 1; FLT: 30 edirector can loop over a chunk of subscribers.
Further Enhancements
Consider adding a eng1; eng1; FLT: 31 eng3; eng3; - an Eloquent model that stores the builder class, tempplate, and default parameters. Then a scheduler jobs reads phaintens andd uses the director to build ingmb; amp; queue emails for all activa subscribers.
You can also introdule a envise 1; Xi1; FLT: 32 contribution 3; Xi3; that applies global rules (like always adding an unsubscribone link) befor e deleging to thee specific builder. Thi adds anothers layer of separation.
External Resources
- Xiv1; Xiv1; FLT: 0 Xiv3; Xiv3; Builder Pattern - Refactoring Gru Xiv1; Xiv1; FLT: 1 Xiv3; Xiv3; (excellent visual Xivation)
- (zob. pkt 2.2.1.1.1 niniejszego załącznika)
- (for scaling email dispatch)
Konkluzja
Designg a explixble email campaign system does note require a massive framework. With the Builder Pattern in Laravel, you gain precise control over email construction, making your codebase adaptable te o chanting markeds. By separating thee what (builder) frem the how (director), you enable teams to add new campaign type with four breakg exiong ones. Combinane this with Laravel 's mail and queue infrastrucure, and yove, testable, testable, anutane, anutie, mainite solute then near near tun your ign comigne for comigne.