Отправка писем

Внезапный дополнительный выпуск в уже оконченном курсе!
Поступил вопрос - а как отправлять письма? Для этого я использую PHPMailer, Fenom и Emogrifier.
Fenom будет готовить шаблоны писем, Emogrifier встраивать стили непосредственно в элементы (чтобы всё правильно читалось в почтовых клиентах), а PHPMailer будет отправлять.
Добавляем новые зависимости в проект:
composer require phpmailer/phpmailer fenom/fenom pelago/emogrifier

Подготовка

Я работаю в Docker и поэтому еще добавляю контейнер с Mailhog в свой docker-compose.yml. Это будет локальный получатель всем писем, для тестирования.:
  mailhog:
    image: teawithfruit/mailhog
    ports:
      - 8090:8025
Дальше все настройки будут указаны для окружения в Docker, так проще.
Теперь добавляем новые переменные окружения: директорию для хранения шаблонов и настройки SMTP. Редактируем наш .env файл:
TEMPLATE_DIR=/vesp/core/templates/

SMTP_HOST=mailhog
SMTP_USER=no-reply@vesp-shop.bezumkin.ru
SMTP_USER_NAME=no-reply
SMTP_PASS=
SMTP_PORT=1025
SMTP_PROTO=
Теперь создаём новый служебный класс src/Services/Fenom.php, он расширит оригинальный Fenom:
<?php

namespace App\Services;

class Fenom extends \Fenom
{
    public function __construct()
    {
        // При инициализации класса передаём файловый провайдер,
        // нацеленный в нашу директорию с шаблонами
        parent::__construct(new \Fenom\Provider(getenv('TEMPLATE_DIR')));

        // Создаём директорию для кэша, если нужно
        $cache = getenv('CACHE_DIR') . 'fenom/';
        if (!file_exists($cache) && !mkdir($cache) && !is_dir($cache)) {
            throw new \RuntimeException(sprintf('Directory "%s" was not created', $cache));
        }
        $this->setCompileDir($cache);

        // Указываем настройки Fenom, согласно документации
        $this->setOptions(self::DENY_NATIVE_FUNCS | self::AUTO_RELOAD | self::FORCE_VERIFY | self::AUTO_ESCAPE);
        $this->addAllowedFunctions(['print_r']);
    }
}
Работа с шаблонизацией окончена. Теперь создаём служебный класс для почты в src/Services/Mail.php:
<?php

namespace App\Services;

use Pelago\Emogrifier\CssInliner;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;

class Mail
{
    public function send(string $to, string $subject, string $tpl, ?array $data = []): ?string
    {
        // Мы работаем только через SMTP, никакой локальной отправки
        // Если хост не указан - значит отправка почты не нужна, просто выходим без ошибки
        if (!getenv('SMTP_HOST')) {
            return null;
        }

        $mail = new PHPMailer(true);
        $mail->CharSet = 'UTF-8';

        // Основные настройки транспорта
        $mail->isSMTP();
        $mail->Host = getenv('SMTP_HOST');
        $mail->SMTPAuth = (bool)getenv('SMTP_USER');
        $mail->Username = getenv('SMTP_USER');
        $mail->Password = getenv('SMTP_PASS');
        $mail->SMTPSecure = getenv('SMTP_PROTO');
        $mail->Port = getenv('SMTP_PORT');
        $mail->SMTPDebug = 0;

        $mail->isHTML();
        $mail->Subject = $subject;

        try {
            // Указываем получателя и отправителя
            $mail->addAddress($to);
            $mail->setFrom(getenv('SMTP_USER'), getenv('SMTP_USER_NAME'));

            // Содержимое письма генерируется из шаблона Fenom
            $body = (new Fenom())->fetch($tpl, $data);

            // При выставлении содержимого, мы распихиваем общие стили HTML по элементам
            $mail->Body = CssInliner::fromHtml($body)->inlineCss()->render();

            // Добавляем версию без HTML, для поддержки древних клиентов - нам не трудно
            $mail->AltBody = $mail->html2text(nl2br($mail->Body));

            // Отправляем
            $mail->send();

            return null;
        } catch (Exception $e) {
            return $mail->ErrorInfo;
        } catch (\Exception $e) {
            return $e->getMessage();
        }
    }
}
В этом классе единственный метод send(), который вернёт или текст ошибки, или null, если отправка прошла успешно.

Работа с шаблонами

Я предпочитаю прописать один-единственный общий шаблон для писем по-умолчанию, чтобы потом его расширять.
Полностью публиковать его не буду, но вот общие правила:
  • Это HTML файл, там что там должен быть <!DOCTYPE HTML>, теги head, body и т.д.
  • Стили пишем внутри тега head в style, они будут разобраны Emogrifier и добавлены элементам в body
  • Вёрстка таблицами, никакого flexbox и CSS 3 - почтовики это не любят.
  • Картинки только JPG и PNG, с абсоютными ссылками.
  • В шаблоне нужно предусмотреть места расширения Fenom тегами {block}
  • Соответственно, везде можно использовать теги Fenom, например для доступа к переменным окружения
То есть, у вас должно быть что-то подобное в core/templates/email.tpl:
<!DOCTYPE HTML>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>{$.env.APP_NAME}</title>
    <style>
        body {
            background: #f7f7f7;
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            font-family: Arial, serif;
            font-size: 14px;
            color: #000;
        }
        {block 'style'}{/block}
    </style>
</head>
<body>
<table>
    <tbody>
    <tr>
        <td class="content">
            {block 'content'}{/block}
        </td>
    </tr>
    <tr>
        <td>
            <a href="{$.env.SITE_URL}" target="_blank">{$.env.APP_NAME}</a>
        </td>
    </tr>
    </tbody>
</table>
</body>
</html>
Общий шаблон есть, теперь нужен шаблончик уведомления о поступившем заказе. Кладём рядом email-order-new.tpl:
{extends 'email.tpl'}

{block 'content'}
    <pre>{print_r($data, true)}</pre>
{/block}
Расширяем и печатаем переменную $data. Дальше сами оформите по вкусу.

Отправка писем

Почти всё готово, осталось добавить отправку пичем в контроллер создания заказа.
    public function put(): ResponseInterface
    {
        // ... до этих строк всё остаётся как было
        if ($order->save()) {
            $order->orderProducts()->createMany($orderProducts);
        }

        // В конце добавляем отправку почты
        if ($order->email) {
            $error = (new \App\Services\Mail())->send(
                $order->email,
                'Спасибо за ваш заказ!',
                'email-order-new.tpl', // шаблон указывается относительно директории Fenom
                ['data' => $order->toArray()] // В шаблоне передаём массив заказа
            );
            // Почта вернёт null, или текст ошибки
            if ($error) {
                return $this->failure($error);
            }
        }

        return $this->success();
    }
Результат смотрим по адресу контейнера Mailhog, или куда вы там письмо отправили:
При просмотре исходников письма можно заметить, что тег style пропал, а сами стили переехали в элементы - это и есть работа Emogrifier.
Вот и вся работа с письмами. Остаётся только отладить шаблоны и вывод содержимого. Добавить товары, картинки и т.д.

4 комментария

Александр Наумов
Василий, спасибо!
А не будут ли письма попадать под спам фильтр если использовать PHPMailer?
Может лучше отправлять письма, как ты учил нас на MODX, через smtp.yandex.ru?
Василий Наумкин
PHPMailer - это просто библиотека для работы с почтой, как через SMTP, так и без. Она же используется и в MODX.
В моём примере без настроек SMTP вообще ничего не отправляется, так что по-любому надо использовать или Яндекс, или Gmail, или что-то еще.
Александр Наумов
Понял, спасибо!
Это сообщение было удалено
bezumkin.ru
Personal website of Vasily Naumkin
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так. А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен. Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. ...
Futuris
04.04.2024, 08:56:12
Я просто немного запутался. Когда в абзаце "Vesp/Core" ты пишешь про "новый trait FileModel", я поду...
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо! Извини, тупанул.
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!