Комментарии 58
Хочу отдельно пояснить как можно реализовать отправку сервисных (транзакционных) писем на yii2. Для этого не обязательно использовать обобщёный класс отправителя Sender, а можно просто реализовать классы RestMessage и UnioneRestMessage*. Затем реализовать класс конкретного письма (NewReserveMailRestMessage).
В итоге для отправки письма можно использовать следующий код
<?php
$client = new \yii\httpclient\Client();
$client->baseUrl = 'https://eu1.unione.io/en/transactional/api/v1/';
$newReserveMail = new NewReserveMailRestMessage($client);
$newReserveMail
->setMethod('POST')
->setUrl('email/send.json')
->setFormat(Client::FORMAT_JSON)
->setApiKey('secret')
->setFormatter(Yii::$app->formatter)
->setSender('Company name')
->setFrom('from@company.email')
->setSubject($subject)
->setEmail($user->email)
->setTemplatePath('/app/mail/unione/user/letter_reserve.php')
->setData([$user, $investment])
$client->send($newReserveMail);
![](https://habrastorage.org/getpro/habr/upload_files/6fe/3c4/f16/6fe3c4f160135bd233e325e6f1f83b54.png)
* можно даже напрямую унаследовать класс UnioneRestMessage от Request, если не планируете использовать отправку других запросов не связанных с апи сервиса. Соответственно код RestMessage нужно будет перенести в UnioneRestMessage.
В данной реализации для создания классов писем используется наследование от компонента фрейворка yii\httpclient\Request, поскольку отправка письма через апи сервиса представляет собой частный случай http запроса к апи сервиса.
Целью статьи является представление практической реализации отправки письма через web api сервиса unione и в ней не рассматриваются проблемы использования наследования для создания новых классов. Общий класс отправителя Sender использован для примера отправки различных сообщений и не обязателен для использования.
Для отправки писем через апи сервиса можно использовать yii\httpclient\Client
Зачем класс RestMessage
знает про Client
? Зачем UnioneRestMessage
наследуется от RestMessage
(и, как следствие, должен знать про Client
)? Какую полезную абстрацию предоставляет класс RestMessage
по сравнению с классом Request
? Где посмотреть на определения интерфейсов?
Зачем класс
RestMessage
знает проClient
?
Это артефакт базового класса Request https://www.yiiframework.com/extension/yiisoft/yii2-httpclient/doc/api/2.0/yii-httpclient-request
Разработчики yii2 так сделали, повторив мою реализацию (или я их). Там даже метод send есть.
Это артефакт базового класса Request
А зачем вам наследоваться от Request
? Зачем вам эта зависимость?
Зачем
UnioneRestMessage
наследуется отRestMessage [...]?
Ответ есть тут:
Здесь (в объекте $newReserveMail - прим. автора) методы setMethod, setUrl и setFormat наследуются от yii\httpclient\Request, setData определён в RestMessage, остальные в UnUnioneRestMessage.
Т.е. я использую уже готовые методы родителя. Их гораздо больше чем у меня в примере.
А зачем пользователю вашего класса вызывать эти методы? Разве метод, адрес и формат сообщение не заданы целиком и полностью АПИ UniOne?
Весь смысл создания программного интерфейса в вашем языке вокруг HTTP API в том, чтобы пользователь класса не знал про это. Пользователь должен вызывать uniOne.sendMail
, а все потроха типа POST <url>
и форматирования в JSON — обязанность вашей обертки.
А зачем пользователю вашего класса вызывать эти методы?
Принял.
Тем не менее ответ на вопрос тот же
Зачем
UnioneRestMessage
наследуется отRestMessage [...]?
Чтобы использовать эти методы, пусть и внутри обертки.
Чтобы использовать эти методы, пусть и внутри обертки.
Чтобы использовать методы, не нужно наследование.
Это вообще распространенная такая ошибка: наследование (в ООП) нужно не для того, чтобы был легкий доступ к методам или для переиспользования кода, наследование нужно в первую очередь для того, чтобы можно было использовать наследник там, где используется базовый класс. Какие места в вашем коде принимают RestMessage
?
<?php
public function send(MessageInterface $message){
/** @var RestMessage|UnioneRestMessage|MailMessage $message */
return $this->client->send($message);
}
Это место принимает не RestMessage
, а MessageInterface
. Почему не реализовать MessageInterface
сразу на UnioneRestMessage
?
Для того чтобы с помощью RestMessage можно было отправлять любые http запросы как в этом примере:
<?php
$client = new RestClient();
$client->baseUrl = 'https://habr.com/';
$restRequest = new RestMessage($client);
$restRequest
->setUrl('ru/post/688090/');
$sender = new Sender($client);
$res = $sender->send($restRequest);
Т.е. класс можно использовать с методом send для отправки любый http запросов к любому апи.
А зачем это нужно? Вы решаете задачу рассылки писем через Unione
, а не написания абстрактного REST-клиента (я не верю, что в вашем фреймворке — или для него — уже нет готового клиента, зачем новый изобретать?).
Более того, даже для задачи отправки любых запросов не нужно наследовать UnioneRestMessage
от RestMessage
, достаточно в них обоих реализовать MessageInterface
.
Но это опять упирается в то, что ваш MessageInteface
сам по себе бесполезен.
А зачем это нужно?
Если я наследуюсь от Request, то логично использовать возможность отправки любых http запросов, потому что этот класс и предназначен для этого. Если не наследоваться то не логично. Но я наследуюсь. Наследование не только для полиморфизма, но и для переиспользования кода.
для задачи отправки любых запросов не нужно наследовать
UnioneRestMessage
отRestMessage
, достаточно в них обоих реализоватьMessageInterface
.
Методы по формированию запроса находятся в Request классе, в UnioneRestMessage их нет, поэтому нужно наследоваться, чтобы из использовать.
Вы решаете задачу рассылки писем через
Unione
, а не написания абстрактного REST-клиента
Пожалуй да. но интересно попробовать что-то большее. Хотя видимо тут есть момент что я не правильно видимо понял ваше предложение вынести метод send из класса Message.
Если я наследуюсь от Request, то логично использовать возможность отправки любых http запросов, потому что этот класс и предназначен для этого.
Нет, не логично. Это не входит в вашу задачу.
Наследование не только для полиморфизма, но и для переиспользования кода.
Вот это как раз фундаментальная ошибка. Наследование в ООП — для полиморфизма. Для переиспользования кода есть другие механизмы.
Методы по формированию запроса находятся в Request классе
А зачем вам в UnioneRestMessage
использовать именно эти методы?
Пожалуй да. но интересно попробовать что-то большее.
Не надо "пробовать большее", пока вы не решили частную задачу. Ваше "большее" мешает вам в решении частного.
Для переиспользования кода есть другие механизмы.
Да, но допустим вы же не предлагаете такое для контролеров, ORM моделей и вьюшек. Когда вы делаете MVC (MVVM) с сразу наследуетесь от них. А можно было бы делать из них композицию и в своих классах без наследования уже вызывать нужные методы. Но вы хотите использовать подготовленную логику без вникания в неё.
Да, но допустим вы же не предлагаете такое для контролеров
Во-первых, это зависит от контроллера (иногда достаточно реализовать интерфейс). Во-вторых, в тех случаях, когда наследование полезно, оно полезно именно потому, что мы переиспользуем не код, а поведение: т.е. каждый контроллер должен обладать вот таким поведением, а мы его расширяем. У вас не так.
Иными словами, я наследуюсь от контроллера не для того, чтобы "вызывать нужные методы", а для того, чтобы фреймворк вызывал нужные методы у базового контроллера, а тот уже роутил их на мои методы. Inversion of Control в чистом виде.
ORM моделей и вьюшек
А вот для моделей наследование от общего предка уже давно антипаттерн. RTFM persistence ignorance.
Какую полезную абстрацию предоставляет класс
RestMessage
по сравнению с классомRequest
?
RestMessage реализует интерфейс MessageInterface чтобы это определение работало для любых $message.
<?php
public function send(MessageInterface $message){
/** @var RestMessage|UnioneRestMessage|MailMessage $message */
return $this->client->send($message);
}
И с помощью него можно отправлять http запросы через общий клиент.
Где посмотреть на определения интерфейсов?
Они пустые. В основном.
Пустой интерфейс не дает полезной абстракции. В чем его смысл?
Смысл в том, что между конкретными юишными реализациями yii\swiftmailer\Message и yii\httpclient\Request нет связи, и чтобы их принимать на вход метода send(MessageInterface $message) нужно их связать общим предком.
П.с. yii\swiftmailer\Message и yii\httpclient\Request стандартные юишные компоненты. В данном случае моя реализация на yii2, поэтому я сделал с использованием стандарных юишных компонентов.
нужно их связать общим предком.
Как вы можете взаимодействовать с этим "общим предком", если у него нет никаких операций и свойств? Как выглядит метод send
?
<?php
public function send(MessageInterface $message){
/** @var RestMessage|UnioneRestMessage|MailMessage $message */
return $this->client->send($message);
}
Что такое client
? Какого оно типа? Какого типа параметр в его методе send
?
Что такое
client
? Какого оно типа?
ClientInterface - /** @var Client|MailSender $client */
<?php
namespace app\services\backend\email;
use app\interfaces\MessageInterface;
use app\interfaces\SenderInterface;
use app\models\Email\MailMessage;
use app\models\Email\RestMessage;
use app\models\Email\UnioneRestMessage;
use app\services\backend\infrastructure\ClientInterface;
use yii\httpclient\Client;
class Sender implements SenderInterface
{
/** @var Client|MailSender $client */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function send(MessageInterface $message)
{
/** @var RestMessage|UnioneRestMessage|MailMessage $message */
return $this->client->send($message);
}
}
<?php
namespace app\services\backend\email;
use app\services\backend\infrastructure\ClientInterface;
use yii\swiftmailer\Mailer;
class MailSender extends Mailer implements ClientInterface
{
public function send($message)
{
$this->sendMessage($message);
}
}
<?php
namespace app\services\backend\infrastructure;
interface ClientInterface
{
public function send($message);
}
interface ClientInterface { public function send($message); }
Какого типа параметр message
?
class MailSender extends Mailer implements ClientInterface { public function send($message) { $this->sendMessage($message); } }
Какого типа параметр message
в операции sendMessage
?
Пожалуй я тут воспользовался возможностью php не указывать тип.
Если тип указывать то там нужно поморочится дополнительно с интерфейсами.
Какого типа параметр
message
в операцииsendMessage
?
\yii\mail\MessageInterface
а вот у yii\http\client -> \yii\httpclient\Request
\yii\mail\MessageInterface
а вот у yii\http\client -> \yii\httpclient\Request
… и что будет, если я передам \yii\httpclient\Request
в MailSender
?
Думаю что тоже самое как если его передали в класс Mailer.
И это значит, что ваша абстрация протекла, а пользователь получил неожиданное поведение. Что намекает нам, что в дизайне ошибка.
ваша абстрация протекла
Абстракция чего?
ClientInterface
. Абстрация чего конкретно это я из вашего кода не очень понимаю.
Есть класс Sender в его конструктор я передаю клиента (почтового или http) и с помощью этого клиента отправляю сообщение (письмо или запрос) в методе send.
Тут видимо абстрагируются вещи которые и в том и в том случае могут быть сделаны в методе send перед отправкой сообщения и после.
Есть класс Sender в его конструктор я передаю клиента (почтового или http) и с помощью этого клиента отправляю сообщение (письмо или запрос) в методе send.
Вот и какой в этом всём смысл? Вашему пользователю все равно надо знать, был там почтовый клиент или http-клиент, чтобы передать соответствующий message.
Тут видимо абстрагируются вещи которые и в том и в том случае могут быть сделаны в методе send перед отправкой сообщения и после.
… которых у вас нет.
Я и говорю: это бессмысленный класс, который ничего не абстрагирует (потому что пользователь должен знать, что было передано), и ничего не добавляет.
Пожалуй я тут воспользовался возможностью php не указывать тип.
Очень зря. Попробуйте как-то в каждом Вашем PHP файле указать declare(strict_types=1);
и типизировать каждый аргумент, каждый return type, каждое свойство класса, заиспользуйте phpstan с уровнем 9, попробуйте решить все ошибки анализатора (он подсказывает, как) и Вы внезапно обнаружите, что язык Вам сам подсказывает где и как лучше сделать.
Спасибо за declare(strict_types=1).
Оно как то поможет использовать чужой готовый Легаси код?
Оно как то поможет использовать чужой готовый Легаси код?
Я не совсем понял, что Вы имеете в виду. В первую очередь строгую типизацию Вы делаете для себя и кода, над которым в данный момент работаете. Оно работает на уровне файла и
применяется к вызовам функций, сделанным изнутри файла с включённой строгой типизацией, а не к функциям, объявленным в этом файле.
Т.е. если Вы не укажете declare(strict_types=1) и будете вызывать функцию из файла с указанной строгой типизацией, то будет применяться приведение типов и Вы сможете передать int вместо string.
То же применимо и к легаси. Если там есть строгая типизация и у Вас включена - всё хорошо, всё будет работать, как ожидается. Если у Вас не включена - то без разницы. Если в легаси не включена - то, скорее, Вам надо бы найти аналог поновее или сделать типизированную обёртку нужных методов (по желанию и возможности, конечно же, я с такой проблемой никогда не сталкивался), или использовать как есть.
Какого типа параметр в его методе
send
?
У меня в интерфейсе тип опущен.
Как вы можете взаимодействовать с этим "общим предком", если у него нет никаких операций и свойств?
Хоршогий вопрос. Тут получается что под нужный message нужен правильный клиент, что как странно...
Тут получается что под нужный message нужен правильный клиент
Что означает, что ваша абстрация протекла насквозь (или, иными словами, вы нарушили LSP). Пустые интерфейсы — вполне узнаваемый code smell.
Фабричный метод мне в помощь? т.е. в потомках MessageInterface сделать createClient?
У меня к вам есть внезапный вопрос: а вы как-то учитываете в дизайне вашего UnioneRestMessage
и его наследников, что Unione
, вообще-то, заточен под отправку множества писем одним вызовом?
Не то чтобы он заточен. Unione как раз заточен под транзакционные письма. Т.е. юзер сделал инвестицию, и ему шлётся письмо. Для рассылок у провайдера сервис Unisender.
Но вы правы, там есть возможность отправлять письма многим получателям. И я её использовал когда, допустим проект собрал полную сумму и всем инвесторам рассылаются письма, что проект запускается.
В моей старой реализации для этого был предусмотрен класс BatchMessage.
<?php
namespace app\models\Email;
use yii\helpers\ArrayHelper;
abstract class BatchMessage extends Message
{
abstract public function getProperties(): array;
abstract public function getGlobalProperties(): array;
abstract public function getSubstitutions(string $email): array;
public function prepareData($data): void
{
foreach ($data as $email => $datum) {
$this->data[$email] = [];
$sub = ArrayHelper::toArray($datum, $this->properties);
foreach ($sub as $el) {
foreach ($el as $key => $value) {
$this->data[$email][$key] = $value;
}
}
}
}
public function getRecipients(string $email = null): array
{
$recipients = [];
foreach ($this->data as $email => $data) {
$recipient = [
'email' => $email,
'substitutions' => $this->getSubstitutions($email)
];
$recipients[] = $recipient;
}
return $recipients;
}
public function getSubstitution(string $name, string $email = null)
{
return $this->data[$email][$name];
}
}
Не было возможности его представить, хорошо что спросили :)
Соответственно наследуясь от него, можно реализовать письмо для многих адрессатов с подстановками для каждого из них, и общие подстановки для всех.
Unione как раз заточен под транзакционные письма. Т.е. юзер сделал инвестицию, и ему шлётся письмо.
… а зачем там тогда массив получателей? Нет, согласно API он заточен под массовую рассылку.
А дальше, собственно начинается очень интересный вопрос, какой же конкретно апи должен предоставлять ваш клиент к этому сервису, и почему.
Соответственно наследуясь от него, можно реализовать письмо для многих адрессатов с подстановками для каждого из них, и общие подстановки для всех.
… вот как раз: если это базовая функциональность сервиса, почему у вас это отдельный класс?
Иными словами, я не понимаю, почему ваше клиентское апи в чем-то повторяет апи Unione, а в чем-то радикально от него отходит.
а зачем там тогда массив получателей? Нет, согласно API он заточен под массовую рассылку.
Мы использовали использовали Unisender для рассылок, в т.ч. единичных писем, и это было не очень юзерфрендли удобно в результате сам сервис рекомендовал свой другой сервис Unione который он и презентовал как сервис для отправки единичных писем назвав их транзакционными. Если вы на основе анализа его апи определили что он заточен пот массовую рассылку, хорошо. перед нами стояла задачи отправлять именно единичные письма одному получателю.
А дальше, собственно начинается очень интересный вопрос, какой же конкретно апи должен предоставлять ваш клиент к этому сервису, и почему.
Думаю, это определяется практической задачей, а не абстрактными рассуждении о программировании. Поэтому он должен предоставлять возможность отправки единичных писем одному адресату и не тянуть при этом тот функционал, который не требуется (т.е. отправку многим адресатам).
Иными словами, я не понимаю, почему ваше клиентское апи в чем-то повторяет апи Unione, а в чем-то радикально от него отходит.
Потому что оно удовлетворяет практической цели отправки одного письма одному адресату без необходимости тянуть то что не нужно. При этом если нужно отправить многим адресатам, то пожалуйста, для этого класс BatchMessage.
Поэтому он должен предоставлять возможность отправки единичных писем одному адресату и не тянуть при этом тот функционал, который не требуется
… а в чем конкретно состоит этот "функционал", который не требуется?
И зачем у вас тогда в UnioneRestMessage
есть метод getRecipients(): array
?
в чем конкретно состоит этот "функционал", который не требуется?
Конкретно в методах getGlobalSubstitutions() и getGlobalProperties() которые имеют смысл если получателей больше одного и задают подстановки сразу для всех получателей.
<?php
namespace app\models\Email;
use app\models\Project\Project;
use app\models\User\User;
use Yii;
class ProjectStatusEmail extends BatchMessage
{
private Project $project;
public function __construct(
Project $project,
string $subject,
bool $useTemplate = false
)
{
parent::__construct($useTemplate);
$this->project = $project;
$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;
},
'invested_in_project' => function (User $user) {
return $user->getActiveInvestmentReserves()->where(['id_project' => $this->project->id])->sum('amount') / 100;
},
'user_funds' => function (User $user) {
$reserves = $user->getActiveInvestmentReserves()->sum('amount');
return ($user->userFunds->amount - $reserves) / 100;
}
],
];
}
public function getSubstitutions(string $email): array
{
return [
"Name" => $this->getSubstitution('fullname', $email),
'Invested_amount' => $this->formatter->asDecimal($this->getSubstitution('invested_in_project', $email), 2),
'Account_balance' => $this->formatter->asDecimal($this->getSubstitution('user_funds', $email), 2)
];
}
public function getGlobalSubstitutions(): array
{
$sub = $this->prepareGlobalData([$this->project]);
$baseFrontUrl = Yii::$app->params['front_url'];
$baseApiUrl = Yii::$app->params['api_url'];
return [
'P1_name' => $this->getGlobalSubstitution($sub, 'name'),
'P1_where' => $this->getGlobalSubstitution($sub, 'country'),
'P1_loan_period' => $this->getGlobalSubstitution($sub, 'loan_period'),
'P1_interest_rate' => $this->getGlobalSubstitution($sub, 'profitability'),
'P1_opisanie' => empty($this->getGlobalSubstitution($sub, 'description')) ? '' : $this->getGlobalSubstitution($sub, 'description'),
'project_page_url' => $baseFrontUrl . '/projects',
'logo' => $baseFrontUrl . '/images/logo.svg',
'front_url' => $baseFrontUrl,
'P1_link_img' => $baseApiUrl . $this->project->image,
'P1_link' => $baseFrontUrl . '/projects/project/' . $this->project->id_label,
'P1_label' => $this->getGlobalSubstitution($sub, 'id_label'),
'P1_target' => $this->formatter->asDecimal($this->getGlobalSubstitution($sub, 'amount'), 2)
];
}
public function getGlobalProperties(): array
{
return [
Project::class => [
'id_label',
'country' => function (Project $project) {
return $project->country->country;
},
'loan_period',
'profitability',
'image' => function (Project $project) {
$base = Yii::$app->params['api_url'];
return $base . $project->image;
},
'name',
'description',
'id_label',
'amount'
]
];
}
public function getOptions(): array
{
return [];
}
}
И зачем у вас тогда в
UnioneRestMessage
есть методgetRecipients()
Вы знаете зачем :) массив получателей там определяется.
Конкретно в методах getGlobalSubstitutions() и getGlobalProperties() которые имеют смысл если получателей больше одного и задают подстановки сразу для всех получателей.
Неа, они имеют смысл всегда. Например, вы можете там задавать те вещи, которые не зависят от конкретного письма (название компании, контактную информацию).
А главное, два дополнительных словарных свойства — это пренебрежимо малая функциональность, ее быстрее реализовать, чем обсуждать, почему она не нужна (и потом все равно убедиться, что от нее есть польза).
Вы знаете зачем :) массив получателей там определяется.
А зачем определять массив получаетелей, если получатель всегда один?
имеют смысл всегда
в случае одного получателя, нет смыла размазывать информацию по нескольким методам. Если у вас другое мнение то попробуйте использовать апи, чтобы примерить ситуацию.
пренебрежимо малая функциональность
Это выглядит так, что когда вам удобно вы отходите от принципов. Входные параметры обрабатываются иначе тоже.
получатель всегда один
"если получатель один"
Если у вас другое мнение то попробуйте использовать апи, чтобы примерить ситуацию.
Да, у меня другое мнение, и я его даже описал уже.
Это выглядит так, что когда вам удобно вы отходите от принципов.
И от какого же принципа я здесь отхожу?..
Но вообще, да, я стараюсь следовать принципам, когда это оправданно — потому что если бездумно следовать им всегда, получается ничуть не лучше, чем если не следовать им вовсе.
"если получатель один"
Вы меня запутали. Если получателей много, то должны быть глобальные подстановки и метаданные. Иными словами, эти свойства должны идти вместе.
Хочу отдельно пояснить как можно реализовать отправку сервисных (транзакционных) писем на yii2. Для этого не обязательно использовать обобщёный класс отправителя Sender, а можно просто реализовать классы RestMessage и UnioneRestMessage*. Затем реализовать класс конкретного письма (NewReserveMailRestMessage).
В итоге для отправки письма можно использовать следующий код
<?php
$client = new \yii\httpclient\Client();
$client->baseUrl = 'https://eu1.unione.io/en/transactional/api/v1/';
$newReserveMail = new NewReserveMailRestMessage($client);
$newReserveMail
->setMethod('POST')
->setUrl('email/send.json')
->setFormat(Client::FORMAT_JSON)
->setApiKey('secret')
->setFormatter(Yii::$app->formatter)
->setSender('Company name')
->setFrom('from@company.email')
->setSubject($subject)
->setEmail($user->email)
->setTemplatePath('/app/mail/unione/user/letter_reserve.php')
->setData([$user, $investment])
$client->send($newReserveMail);
![](https://habrastorage.org/getpro/habr/upload_files/6fe/3c4/f16/6fe3c4f160135bd233e325e6f1f83b54.png)
* можно даже напрямую унаследовать класс UnioneRestMessage от Request, если не планируете использовать отправку других запросов не связанных с апи сервиса. Соответственно код RestMessage нужно будет перенести в UnioneRestMessage.
В данной реализации для создания классов писем используется наследование от компонента фрейворка yii\httpclient\Request, поскольку отправка письма через апи сервиса представляет собой частный случай http запроса к апи сервиса.
Целью статьи является представление практической реализации отправки письма через web api сервиса unione и в ней не рассматриваются проблемы использования наследования для создания новых классов. Общий класс отправителя Sender использован для примера отправки различных сообщений и не обязателен для использования.
Для отправки писем через апи сервиса можно использовать yii\httpclient\Client
2022 год, yii2.. ну...
Рассылка писем через Unione (php, Yii2)