Разработка гибкой системы электронной почты с шаблоном строителя в Laravel

Современный маркетинг требует почтовых кампаний, которые быстро адаптируются к сегментам аудитории, A/B-тестов и динамического контента. Жесткая, монолитная реализация быстро становится кошмаром обслуживания по мере развития требований. Laravel с его выразительным синтаксисом и надежной экосистемой обеспечивает идеальную основу, но реальный ключ к долгосрочной гибкости заключается в выборе правильного шаблона дизайна. Модель Builder выделяется как мощное решение для построения сложных объектов электронной почты шаг за шагом, позволяя менять компоненты без переписывания основной логики.

В этом всеобъемлющем руководстве вы узнаете, как создать гибкую систему кампании по электронной почте в Laravel с использованием шаблона Builder. Мы разберем теорию, пройдемся по подробной реализации и изучим интеграцию с системами рассылки и очереди Laravel. К концу у вас будет готовый к производству подход к созданию любого варианта кампании по электронной почте - от транзакционных сообщений в простом тексте до богатых рекламных акций HTML - с минимальным дублированием кода.

Что такое шаблон строителя?

Шаблон Строителя — это шаблон креационного дизайна, который отделяет конструкцию сложного объекта от его окончательного представления. Вместо того, чтобы создавать объект с помощью массивного конструктора или набора заводских методов, вы делегируете процесс строительства специальному директору и классам строителей. Директор организует шаги, в то время как каждый строитель знает, как собрать компоненты для конкретного варианта.

Этот шаблон сияет, когда объект требует много дополнительных частей, имеет несколько этапов конфигурации или когда вам нужно создавать различные представления похожих объектов. Для кампаний по электронной почте «объект» - это сообщение электронной почты - в комплекте с субъектом, телом, вложениями, получателями, заголовками и метаданными. Каждый тип кампании (рекламный, транзакционный, спровоцированный событиями) может иметь общую базу, но отличаться по макету, информации об отправителе или блокам контента.

Чем он отличается от фабричного шаблона

В то время как шаблон завода фокусируется на создании объектов в один звонок, шаблон строителя позволяет контролируемый, по частям процесс строительства. Фабрики идеальны, когда логика создания проста; строители лучше, когда вам нужно контролировать порядок и выбор деталей. В системах электронной почты вам часто нужно условно добавлять вложения, изменять шаблон тела или устанавливать различные приоритеты доставки. Паттерн строителя дает вам этот гранулированный контроль без вздутия одного класса завода.

Создайте свою среду Laravel

Прежде чем погрузиться в код, убедитесь, что у вас есть приложение Laravel (версия 9 или более поздняя) с конфигурацией почты по умолчанию. Мы предположим, что у вас есть необходимые таблицы баз данных для кампаний, подписчиков и шаблонов, но для этой статьи мы сосредоточимся на самой логике конструктора. Вы можете следовать вместе со свежей установкой Laravel с помощью Composer:

composer create-project laravel/laravel email-campaign-builder

Затем настройте свой почтовый драйвер в (например, для тестирования). Мы также будем использовать фасад Laravel и встроенные классы позже, но наш строитель останется независимым от них, чтобы поддерживать чистое разделение.

Основные компоненты шаблона строителя

Мы будем реализовывать четыре основных компонента:

  1. Продукт — конечный объект электронной почты (может быть простым , , или расширением объекта Laravel ).
  2. Интерфейс разработчика — Объявляет методы для каждой части электронной почты: субъект, тело, получатели, вложения, заголовки и т. д.
  3. Конкретные конструкторы — каждый реализует интерфейс для определенного типа электронной почты (Promotional, Transactional, Welcome series).
  4. Директор — оркеструет этапы строительства в определенном порядке, часто используя один и тот же конструктор для создания нескольких электронных писем из чертежа.

Шаг 1: Определите продукт электронной почты

Мы создадим простой объект ценности для хранения всех данных электронной почты. Это сохраняет наш код конструктора чистым и проверяемым.

<?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],
 ];
 }
}

Шаг 2: Создайте интерфейс

Интерфейс определяет контракт на создание любого варианта электронной почты.

<?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;
}

Шаг 3: Конкретный конструктор для рекламных писем

Давайте реализуем конструктор, который адаптирует электронную почту для рекламных акций - добавление пикселей отслеживания, ссылок на социальные акции и стандартного нижний колонтитул отписки.

<?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();
 }
}

Шаг 4: пример транзакционного строителя

Транзакционное электронное письмо (например, подтверждение заказа) нуждается в другой обертке - минимальном брендинге, заголовках приоритетов и отсутствии подписки.

<?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
}

Исполнитель: Orchestrating the Build

Директор берет пример конструктора и называет его шаги в определенном порядке. Именно здесь можно определить стандартные последовательности, такие как «построить электронное письмо кампании для данного абонента».

<?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();
 }
}

Использование директора позволяет централизовать строительную логику.Если вам позже нужно добавить метод , вы просто расширяете директора, не касаясь строителей.

Интеграция с Laravel Mail

После того, как у вас есть объект, вы должны отправить его. Создайте пользовательский Mailable, который принимает и отображает его с помощью встроенной почтовой системы 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);
 }
}

Теперь вы можете отправлять электронные письма от любого контроллера или работы с помощью конструктора:

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));

Использование очереди для масштабируемости

Системы кампании электронной почты должны обрабатывать тысячи получателей асинхронно. Система очереди Laravel идеально подходит. Оберните логику отправки в очерченную работу, которая использует конструктор для каждого получателя.

<?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));
 }
}

Отправляйте работу для каждого абонента в цикле (или лучше используйте для управления успешными / неудавшимися отправками).

Добавление динамических шаблонов с помощью Blade

Вместо этого, передайте визуализированные представления Blade в виде тела. Ваш конструктор может принять имя просмотра и массив данных, а затем позвоните внутри . Это делает ваши шаблоны отдельными и легкими для редактирования дизайнерами.

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;
}

Вы можете позвонить [[23]].

Преимущества в реальных кампаниях

Лучшие практики и общие подводные камни

Держите строителей без гражданства, где это возможно

Метод FLT:25 гарантирует, что конструктор может быть повторно использован. Если вы забудете позвонить , один и тот же экземпляр конструктора может протекать между различными кампаниями. В нашем примере звонки автоматически - безопасный шаблон.

Не переусердствуйте с простыми электронными письмами

Если ваше приложение отправляет только один вид электронной почты, шаблон конструктора может быть избыточным. Он сияет, когда у вас есть по крайней мере три различных типа электронной почты с различными компонентами.

Используйте инъекцию зависимости для строителей

Зарегистрируйте своих строителей в служебном контейнере, чтобы вы могли легко вводить в них зависимости (например, услуги регистрации или отслеживания).

Подумайте о количестве получателей

Строитель не должен собирать тысячи получателей в одном — это загружало бы гигабайт в память. Вместо этого создайте одно электронное письмо на получателя или используйте пакетные API (]).

Дальнейшее совершенствование

Подумайте о добавлении — красноречивой модели, которая хранит класс строителя, шаблон и параметры по умолчанию. Затем задание планировщика считывает чертежи и использует директора для создания & очереди электронных писем для всех активных подписчиков.

Вы также можете ввести , который применяет глобальные правила (например, всегда добавляя ссылку для отказа от подписки) перед делегированием конкретному строителю.

Внешние ресурсы

Заключение

Разработка гибкой системы кампаний по электронной почте не требует масштабной структуры. С помощью шаблона Builder в Laravel вы получаете точный контроль над конструкцией электронной почты, что делает вашу кодовую базу адаптируемой к меняющимся маркетинговым потребностям. Разделяя то, что (строитель) от того, как (директор), вы позволяете командам добавлять новые типы кампаний, не опасаясь взлома существующих. Объедините это с инфраструктурой почты и очередей Laravel, и у вас есть масштабируемое, тестируемое и поддерживающее решение, которое будет служить вашим кампаниям в течение многих лет.