В данной статье мы бы хотели описать использованный нами ООП подход к отправке писем через данный сервис рассылок на php. Суть которого заключается в создании объекта инкапсулирующего данные необходимые для отправки письма и методы работы с ними. Кажется что это довольно простая идея, но нам не встречалось ещё в доступных источниках такого подхода, поэтому хотим внести свой вклад в этом направлении. Тем не менее статья написана не для того чтобы пропагандировать данный подход. Она написана с целью развития компетенций в области ООП и способности его использования для практических задач, будь то отправка писем или ещё что-то.
С методом апи сервиса для отправки письма можно ознакомится по данной ссылке.
После обсуждения в комментария мы пришли к тому, что следовало сделать следующим образом:
<?php
$newReserveMail = new NewReserveMail();
$newReserveMail
->setEmail($user->email)
->setSubject($subject)
->setTemplate('/app/mail/unione/user/letter_reserve.php')
->setContent([$user, $investment])
$res = $mailer->sendMessage($newReserveMail)
С учётом данного замечания хотел бы добавить, что я хотел бы чтобы вы обратили внимание в данной статье конкретно на использование базового класса, формирование тела запроса через шаблонный метод и определение в наследнике только подстановок, остальное не следует воспринимать как правильную реализацию.
Для начала давайте рассмотрим применение такого объекта для отправки письма на примере письма об успешном резерве средств при инвестировании в проект (здесь и далее код приводится в контексте 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. К его преимуществам можно отнести то, что вся логика по составлению тела запроса к сервису рассылок находится в одном месте в базовом классе, что позволяет править её только в одном месте сразу для всех классов писем. Добавление же новых типов писем заключается в создании наследника и определении в нем данных которые нужно извлечь и которые нужно подставить в шаблон тела письма, что уменьшает вероятность ошибки при составлении тела запроса к сервису и при надлежащем освоении реализации может упростить и создание новых писем для не знакомого с апи сервиса рассылок пользователя.