Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Пошта

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

6 питань

Лист описується класом Mailable, шаблон - Blade або Markdown.

php artisan make:mail WelcomeEmail --markdown=emails.welcome
Mail::to($user)->send(new WelcomeEmail($user));

Налаштування транспорту (SMTP, Mailgun, SES, log) - у config/mail.php та .env. Реалізувавши ShouldQueue на Mailable, відправку можна винести в чергу, щоб не блокувати запит.

Докладніше в документації: Mail

Mailable - клас, що описує один тип листа: тему, відправника, шаблон, дані й вкладення. Створюється командою:

php artisan make:mail OrderShipped

Три основні методи:

use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\{Address, Attachment, Content, Envelope};

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            from: new Address('shop@example.com', 'Acme Shop'),
            replyTo: [new Address('support@example.com')],
            subject: "Замовлення №{$this->order->number} відправлено",
        );
    }

    public function content(): Content
    {
        return new Content(
            view: 'mail.orders.shipped',
            text: 'mail.orders.shipped-text',
        );
    }

    public function attachments(): array
    {
        return [
            Attachment::fromStorageDisk('s3', $this->order->invoice_path)
                ->as('invoice.pdf')
                ->withMime('application/pdf'),
        ];
    }
}
Метод Що визначає
envelope() тема, відправник, reply-to, теги й метадані для провайдера
content() Blade-шаблон HTML-версії, текстова версія, Markdown-шаблон
attachments() вкладення з диска, сховища чи згенерованих даних

Дані для шаблону: усі публічні властивості класу автоматично доступні у шаблоні ($order). Для обчислених значень - параметр with у Content.

Надсилання:

Mail::to($order->customer)
    ->cc($order->manager)
    ->send(new OrderShipped($order));

to() приймає email, модель з полями email і name або колекцію.

Глобальний відправник: щоб не вказувати from у кожному листі - MAIL_FROM_ADDRESS і MAIL_FROM_NAME в .env.

Корисні звички:

  • текстова версія листа покращує доставлюваність і читання в простих клієнтах;
  • великі файли краще не вкладати, а давати посилання (наприклад, підписаний URL);
  • лист можна повернути з маршруту (return new OrderShipped($order);) і переглянути в браузері під час розробки.

Докладніше в документації: Пошта: написання mailable-класів

Тест не повинен слати справжні листи - це повільно, ненадійно й іноді доходить до реальних людей.

Mail::fake() підміняє транспорт і запамʼятовує, що мало піти:

it('sends a welcome letter on registration', function () {
    Mail::fake();

    $this->post('/register', [
        'email' => 'dev@example.com',
        'password' => 'password',
    ]);

    Mail::assertSent(WelcomeMail::class, function (WelcomeMail $mail) {
        return $mail->hasTo('dev@example.com');
    });
});

Є ще assertNotSent(), assertNothingSent() і assertSentCount().

Важлива пастка: якщо лист відправляється через сповіщення ($user->notify(...)), Mail::fake() його не побачить - потрібен Notification::fake() і assertSentTo(). Плутанина між цими двома - найчастіша причина «тест не бачить листа, хоча він точно йде».

Друга пастка: якщо mailable реалізує ShouldQueue, а в тесті стоїть Queue::fake(), лист не дійде до пошти взагалі - перевіряти треба постановку завдання.

Поза тестами для перегляду верстки зручні два інструменти. Драйвер log пише лист у storage/logs, а Mailpit чи Mailtrap ловлять пошту в локальний ящик - листи виглядають як справжні, але нікуди не йдуть.

Ще одна дрібниця: mailable можна відкрити прямо в браузері, повернувши його з маршруту - зручно для правки шаблону без повторних відправлень.

Докладніше в документації: Пошта

Markdown-листи - шаблони з готовими компонентами, які Laravel перетворює на адаптивний HTML з вбудованими стилями й одночасно на текстову версію:

php artisan make:mail OrderShipped --markdown=mail.orders.shipped
<x-mail::message>
# Замовлення відправлено

Ваше замовлення №{{ $order->number }} вже в дорозі.

<x-mail::button :url="$trackingUrl">
Відстежити посилку
</x-mail::button>

<x-mail::table>
| Товар | Кількість |
|:------|----------:|
@foreach ($order->items as $item)
| {{ $item->name }} | {{ $item->quantity }} |
@endforeach
</x-mail::table>

Дякуємо,<br>{{ config('app.name') }}
</x-mail::message>

Компоненти й CSS можна опублікувати (vendor:publish --tag=laravel-mail) і змінити під свій бренд. Головна перевага - не потрібно вручну писати табличну верстку, яку вимагають поштові клієнти.

Листи в черзі. Надсилання через SMTP чи API провайдера займає сотні мілісекунд і може впасти. Користувач не має на це чекати:

Mail::to($user)->queue(new OrderShipped($order));
Mail::to($user)->later(now()->addMinutes(10), new OrderShipped($order));

Або завжди в черзі - інтерфейс ShouldQueue на класі, і тоді навіть send() ставить лист у чергу.

SerializesModels у черзі зберігає лише ідентифікатор моделі, а воркер перечитує модель з бази перед надсиланням.

Навіщо afterCommit:

DB::transaction(function () use ($order) {
    $order->update(['status' => 'shipped']);
    Mail::to($order->customer)->queue(new OrderShipped($order));
    // ... ще операції, які можуть впасти
});

Проблеми без нього:

  • воркер може взяти завдання до коміту транзакції - модель ще не оновлена чи взагалі не існує, і лист міститиме старі дані або завдання впаде з ModelNotFoundException;
  • якщо транзакція відкотилася, лист про «відправлене замовлення» все одно піде.
Mail::to($order->customer)->queue((new OrderShipped($order))->afterCommit());

Тепер лист ставиться в чергу лише після успішного коміту, а при відкаті - не ставиться взагалі. Глобально це вмикає after_commit => true у конфігурації підключення черги.

Обробка збоїв: невдалі листи потрапляють у failed_jobs; можна задати $tries і backoff на класі листа, як у звичайного завдання.

Докладніше в документації: Пошта: листи в черзі й транзакції

Більшість причин лежить поза кодом - у DNS і репутації домену, - але частина залежить від застосунку.

Що налаштовується в DNS:

  • SPF - перелік серверів, яким дозволено слати від імені домену.
  • DKIM - криптопідпис листа; без нього провайдер не може підтвердити, що лист не підроблено.
  • DMARC - політика на випадок, коли SPF чи DKIM не зійшлися.

Без цих трьох записів лист від нового домену з високою ймовірністю не дійде до вхідних.

Що залежить від застосунку:

  • Адреса відправника з власного домену. from виду noreply@gmail.com не пройде перевірку - домен у from має збігатися з тим, для якого налаштовані SPF і DKIM.
  • Транзакційне окремо від масового. Розсилка з поганим показником відкриттів псує репутацію домену, і за нею перестають доходити листи про відновлення пароля. Часто для розсилок беруть окремий піддомен.
  • Відписка. Для масових листів заголовок List-Unsubscribe обовʼязковий: без нього люди тиснуть «спам» замість відписки, і це найгірший сигнал.
  • Текстова версія. Лист без text/plain частина фільтрів вважає підозрілим.
  • Обробка bounce. Слати на адресу, що повертає помилку, місяцями - прямий шлях до блокування; провайдери дають вебхуки, які варто слухати.

Практично: транзакційну пошту віддають спеціалізованому сервісу (Mailgun, Postmark, SES), а не SMTP власного сервера, - вони тримають репутацію IP і дають статистику доставки. У Laravel це лише зміна MAIL_MAILER.

Докладніше в документації: Пошта

Пошта - зовнішня залежність: провайдер може лежати, обмежувати частоту, заблокувати акаунт за скарги. Для листів, від яких залежить бізнес (підтвердження, скидання пароля, рахунки), потрібен план на ці випадки.

1. Failover - резервні транспорти:

// config/mail.php
'mailers' => [
    'failover' => [
        'transport' => 'failover',
        'mailers' => ['postmark', 'mailgun', 'sendmail'],
        'retry_after' => 60,
    ],
],
MAIL_MAILER=failover

Якщо postmark повертає помилку, лист іде через mailgun. Транспорт, що впав, позначається недоступним на retry_after секунд - наступні листи одразу йдуть через резервний. Механізм побудований на FailoverTransport із Symfony Mailer.

2. Round robin - розподіл навантаження:

'roundrobin' => [
    'transport' => 'roundrobin',
    'mailers' => ['ses', 'postmark'],
],

Листи розподіляються між провайдерами по черзі, а недоступний пропускається.

Що варто врахувати з кількома провайдерами: кожен потребує налаштованих SPF, DKIM і домену відправника, інакше резервні листи потраплять у спам. Вебхуки про відмови й скарги (bounces, complaints) теж треба приймати від усіх.

3. Різні мейлери для різних листів:

Mail::mailer('postmark')->to($user)->send(new PasswordReset($token));   // транзакційні
Mail::mailer('ses')->to($subscribers)->queue(new Newsletter($issue));   // масові

Масова розсилка не повинна псувати репутацію домену чи вичерпувати ліміт, потрібний для транзакційних листів. Часто для розсилок використовують окремий піддомен.

4. Ліміти частоти. Провайдери обмежують кількість листів за секунду чи добу. Laravel не обмежує сам - це робиться на рівні черги:

  • окрема черга mail з фіксованою кількістю воркерів;
  • middleware завдань RateLimited чи Redis::throttle() для листів у черзі;
  • $tries і backoff, щоб тимчасові помилки (429, 5xx) повторювалися з затримкою.

5. Спостереження й налагодження:

  • події MessageSending і MessageSent - для журналу надісланого чи блокування листів за умовою;
  • перегляд у браузері під час розробки: маршрут, що повертає mailable, рендерить його як сторінку;
  • на staging - драйвер log чи Mailpit/Mailtrap, а також Mail::alwaysTo() у середовищі, щоб тестові листи не потрапили справжнім клієнтам.

6. Тести: Mail::fake() і assertQueued перевіряють, що лист поставлено в чергу з правильними даними, а assertSeeInHtml - зміст листа.

Докладніше в документації: Пошта: конфігурація failover