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

Внезапный дополнительный выпуск в уже оконченном курсе!

Поступил вопрос - а как отправлять письма? Для этого я использую 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@shop.vesp.pro
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)
bezumkinВасилий Наумкин
31.05.2023 15:52

PHPMailer - это просто библиотека для работы с почтой, как через SMTP, так и без. Она же используется и в MODX.

В моём примере без настроек SMTP вообще ничего не отправляется, так что по-любому надо использовать или Яндекс, или Gmail, или что-то еще.

Это сообщение было удалено
futuris
Futuris
26.03.2024 07:39
Страница отдельного поста заработала сразу в том виде, как ты написал.) А вот в ленте постов контент...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...
bezumkin
Василий Наумкин
22.11.2023 08:09
Отлично, поздравляю!