В данной статье мы бы хотели описать использованный нами ООП подход к отправке писем через данный сервис рассылок на 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/
Добавить комментарий