Использование ООП подхода для рассылки писем через Unione (php, Yii2)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В данной статье мы бы хотели описать использованный нами ООП подход к отправке писем через данный сервис рассылок на php. Суть которого заключается в создании объекта инкапсулирующего данные необходимые для отправки письма и методы работы с ними. Кажется что это довольно простая идея, но нам не встречалось ещё в доступных источниках такого подхода, поэтому хотим внести свой вклад в этом направлении. Тем не менее статья написана не для того чтобы пропагандировать данный подход. Она написана с целью развития компетенций в области ООП и способности его использования практических задач, будь то отправка писем или ещё что-то.

С методом апи сервиса для отправки письма можно ознакомится по данной ссылке.

Для начала давайте рассмотрим применение такого объекта для отправки письма на примере письма об успешном резерве средств при инвестировании в проект (здесь и далее код приводится в контексте Yii2, однако его (код) довольно легко адаптировать под другую среду.).

<?php

namespace app\jobs\mailing;

use app\models\Email\NewReserveMail;

$newReserveMail = new NewReserveMail($user->email, $subject);
$res = $newReserveMail->sendMessage([$user, $investment], '/app/mail/unione/user/letter_reserve.php');

Тут мы создаём объект письма передав в его конструктор емейл адрес получателя и тему письма. После этого с помощью метода sendMessage происходит отправка письма. В этот метод передаётся массив объектов из которых извлекаются данные для письма и путь к сохранённому файлу шаблона письма составленного на самом сервисе с помощью его конструктора.

Теперь давайте посмотрим на сам объект письма и как в нём извлекаются данные из входных объектов.

<?php

namespace app\models\Email;

use app\models\Investment\InvestmentReserve;
use app\models\User\User;
use Yii;

/**
 * New investment reserve message
 *
 * @property-read array $properties
 * @property-read array $substitutions
 */
class NewReserveMail extends Message
{
    public function __construct(string $email, string $subject, bool $useTemplate = false)
    {
        parent::__construct($useTemplate);
        $this->email    = $email;
        $this->subject  = $subject;
        $this->from     = Yii::$app->params['unione']['from'];
        $this->sender   = Yii::$app->params['unione']['name'];
        $this->template = Yii::$app->params['unione']['templates']['new_reserve']['template_id'];
    }

    public function getProperties(): array
    {
        return [
            User::class => [
                'fullname' => function (User $user) {
                    return $user->fullName;
                },
                'user_funds' => function (User $user) {
                    $reserved = array_reduce($user->activeInvestmentReserves, function (int $sum, InvestmentReserve $reserve) {
                        return $sum + $reserve->amount;
                    }, 0);
                    return $this->formatter->asDecimal(($user->userFunds->amount - $reserved)/100, 2);
                }
            ],
            InvestmentReserve::class => [
                'amount' => function (InvestmentReserve $reserve) {
                    return $this->formatter->asDecimal($reserve->amount / 100, 2);
                },
                'name' => function (InvestmentReserve $reserve) {
                    return $reserve->project->name;
                }
            ]
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function getSubstitutions(string $email = null): array
    {
        return [
            "Name"            => $this->getSubstitution('fullname'),
            "Invested_amount" => $this->getSubstitution('amount'),
            "name"            => $this->getSubstitution('name'),
            "Account_balance" => $this->getSubstitution('user_funds')
        ];
    }

    public function getOptions(): array
    {
        return [];
    }
}

В шаблоне письма присутствуют т.н. подстановки определяемые с помощью двойных фигурных скобочек {{подстановка}}. В приведённом выше методе NewReserveMail::getSubstitutions как раз определяются значения для этих подстановок, которые сервис использует при создании тела письма. В методе NewReserveMail::getProperties определяются данные, которые будут извлекаться из массива входных объектов в методе NewReserveMail::sendMessage с помощью вспомогательной утилиты ArrayHelper::toArray. Эти данные доступны через метод NewReserveMail::getSubstitution. Например, $this->getSubstitution('fullname') получает значение fullname из объекта User.

Таким образом, создание новых типов писем сводится к созданию новый классов писем и определения в них методов getProperties и getSubstitutions с помощью которых определяются данные которые нужно извлечь из входных параметров и определяются подстановки для шаблона тела письма соответственно. Остальная логика заключена в базовом классе Message от которого они наследуются. И прежде чем рассмотреть его взглянем на шаблон тела письма.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">

<head>
 
</head>

<body>
<p>
Dear {{ Name }},
You've invested {{Invested_amount}} in the {{name}} project.
Your balance is {{Account_balance}}.
</p>
</body>

</html>

Теперь рассмотрим базовый класс Message благодаря которому и имеет место быть использованный ООП подход.

<?php

namespace app\models\Email;

use app\services\backend\email\EmailServiceInterface;
use app\services\backend\email\UniOneService;
use Throwable;
use Yii;
use yii\base\Model;
use yii\helpers\ArrayHelper;
use yii\httpclient\Exception;
use yii\httpclient\Response;
use yii\web\View;

/**
 * @property-read array $recipients
 * @property-read array $globalSubstitutions
 */
abstract class Message extends Model
{
    protected array  $data;
    protected $template;
    protected $email;
    protected $subject;
    protected $from;
    protected $sender;
    protected $formatter;
    protected $useTemplate;

    /**
     * @var UniOneService
     */
    private $mailer;

    /**
     * Determines properties to be extracted from input objects
     * @return array
     */
    abstract public function getProperties(): array;

    /**
     * Determines substitution array for unione message body
     * @return array
     */
    abstract public function getSubstitutions(string $email): array;
    abstract public function getOptions(): array;

    /**
     * Constructor
     */
    public function __construct(bool $useTemplate)
    {
        parent::__construct();

        $this->useTemplate = $useTemplate;

        try {
            $this->mailer = Yii::$container->get(EmailServiceInterface::class);
            $apiKey = Yii::$app->params['unione']['apiKey'];
            $baseUrl = Yii::$app->params['unione']['baseUrl'];
            $this->formatter = Yii::$app->formatter;
            $this->mailer->initClient($apiKey, $baseUrl);

            $this->from       = Yii::$app->params['unione']['from'];
            $this->sender     = Yii::$app->params['unione']['name'];
        } catch (Throwable $e) {
            Yii::error($e->getMessage(), 'app');
        }
    }

    /**
     * Send message to the recipient
     * @throws Exception
     */
    public function sendMessage(array $data, string $path = null): Response
    {
        $this->prepareData($data);

        return $this->mailer->sendEmail($this->composeMessage($path));
    }

    /**
     * Prepares data to be input into a message
     * @param $data
     */
    public function prepareData($data): void
    {
        $sub = ArrayHelper::toArray($data, $this->properties);
        foreach ($sub as $el) {
            foreach ($el as $key => $value) {
                $this->data[$key] = $value;
            }
        }
    }

    /**
     * Return ready to use array to send to the recipient
     * @return array[]
     */
    public function composeMessage(string $path): array
    {
        $message = [
            "message" => [
                "recipients" => $this->recipients,
                "subject"       => $this->subject,
                "from_email"    => $this->from,
                "from_name"     => $this->sender,
                'global_substitutions' => $this->getGlobalSubstitutions(),
                'options'              => $this->getOptions(),
//                "skip_unsubscribe"     => 0
            ]
        ];
        if ($this->useTemplate) {
            $message['message']['template_id'] = $this->template;

        } else {
            $message['message']['body'] = [
                "html"  => $this->render($path)
            ];
        }
        return $message;
    }

    public function getRecipients(string $email = null): array
    {
        $recipients = [
            [
                "email"  => $email? $email : $this->email,
                "substitutions" => $this->substitutions
            ]
        ];
        return $recipients;
    }
	
    /**
     * Gets value from prepared data by the key
     */
    public function getSubstitution(string $name, string $email = null)
    {
        return $this->data[$name];
    }

    public function getGlobalSubstitution(array &$data, string $name)
    {
        return $data[$name];
    }

    public function render(string  $path)
    {
        $view = new View();
        return $view->renderFile($path, $this->getRenderVariables());
    }

    public function getGlobalSubstitutions(): array
    {
        return [];
    }

    public function prepareGlobalData(array $data): array
    {
        $sub = ArrayHelper::toArray($data, $this->globalProperties);
        $subData = [];
        foreach ($sub as $el) {
            foreach ($el as $key => $value) {
                $subData[$key] = $value;
            }
        }
        return $subData;
    }

    public function getRenderVariables(): array
    {
        return $this->getGlobalSubstitutions();
    }
}

Собственно говоря, тут два основных метода. Это Message::prepareData - с помощью которого обрабатываются данные из входных объектов. И метод Message::composeMessage с помощью которого формируется тело http запроса к сервису рассылок. Все остальные методы используются им для того чтобы заполнять соответствующие значения ключей массива данных запроса. $this->mailer -представляет собой реализацию отправки http запросов к сервису рассылок с помощью Yii2 HttpClient\Client клиента.

Нами рассмотрена реализация ОПП подхода к отправке писем через сервис рассылок Unione. К его преимуществам можно отнести то, что вся логика по составлению тела запроса к сервису рассылок находится в одном месте в базовом классе, что позволяет править её только в одном месте сразу для всех классов писем. Добавление же новых типов писем заключается в создании наследника и определении в нем данных которые нужно извлечь и которые нужно подставить в шаблон тела письма, что уменьшает вероятность ошибки при составлении тела запроса к сервису и при надлежащем освоении реализации может упростить и создание новых писем для не знакомого с апи сервиса рассылок пользователя.

Источник: https://habr.com/ru/post/688090/


Интересные статьи

Интересные статьи

Как ускорить 1С БИТРИКС и снизить кол-во запросов к БД используя ядро D7.Пример выборки элементов IBlock с пользовательскими свойствами в один запрос.
В курсе анализа данных есть методика исследования зависимостей между данными через корреляцию Пирсона. Корреляция тем выше чем ряды данных более связаны между собой. И наоборот. Не плохо было бы приме...
В perl есть выражение local. Оно подменяет указанное значение undef-ом до конца блока. В качестве значения могут выступать глобальные хеши, массивы и скаляры, а так же элементы или срезы хешей и скаля...
Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox. В данной статье брутфорсим пароль от SMB и повышаем привилегии до администратора от имени ч...
Однажды в одном из проектов в мои руки попал фискальный принтер. Мы каждый день сталкиваемся с этими устройствами, когда совершаем платежи в магазинах, но мало кто догадывается что на самом деле ...