Как реализовать отправку огромного числа писем с помощью класса, принимающего один email и отправляющего письмо?

«Как реализовать отправку огромного числа писем с помощью класса, принимающего один email и отправляющего письмо?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Прямой последовательный вызов метода отправки для каждого email в цикле неэффективен и приведёт к таймаутам. Я решаю эту задачу, используя асинхронную обработку через очередь заданий (Job Queue).

1. Базовый класс для отправки одного письма (например, в Laravel):

// AppMailNewsletterMailer.php
namespace AppMail;

use AppModelsSubscriber;

class NewsletterMailer
{
    public function send(string $email, string $content): bool
    {
        // Логика отправки одного письма через SMTP, Mailgun, SendGrid и т.д.
        // Например, использование встроенного Laravel Mail
        Mail::to($email)->send(new AppMailNewsletter($content));

        // Логируем результат или обрабатываем ошибки
        Log::info("Newsletter sent to: {$email}");
        return true;
    }
}

2. Создание задания для очереди:

// AppJobsSendNewsletterJob.php
namespace AppJobs;

use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use AppMailNewsletterMailer;

class SendNewsletterJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public string $email,
        public string $content
    ) {}

    public function handle(NewsletterMailer $mailer): void
    {
        // Отправляем одно письмо
        $mailer->send($this->email, $this->content);
    }
}

3. Постановка всех писем в очередь (например, в команде Artisan):

// AppConsoleCommandsSendNewsletter.php
public function handle()
{
    $subscribers = Subscriber::where('is_active', true)->cursor(); // Используем курсор для экономии памяти
    $content = $this->getNewsletterContent();

    foreach ($subscribers as $subscriber) {
        // Вместо немедленной отправки — кладём задание в очередь
        SendNewsletterJob::dispatch($subscriber->email, $content);
        // Можно использовать задержку для регулировки нагрузки
        // SendNewsletterJob::dispatch($subscriber->email, $content)->delay(now()->addSeconds(5));
    }

    $this->info('All newsletter jobs have been queued.');
}

Преимущества такого подхода:

  • Масштабируемость: Воркеры могут обрабатывать очередь параллельно.
  • Отказоустойчивость: Если отправка одного письма упадёт, задание можно повторить.
  • Производительность: HTTP-запрос или консольная команда завершается мгновенно, не дожидаясь отправки тысяч писем.
  • Контроль нагрузки: Можно регулировать количество воркеров и скорость обработки.

Для отправки миллионов писем лучше использовать специализированные сервисы (SendGrid, Amazon SES), которые предоставляют свои API и очереди.