В данной статье мы бы хотели описать использованный нами ООП подход к отправке писем через данный сервис рассылок на 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. К его преимуществам можно отнести то, что вся логика по составлению тела запроса к сервису рассылок находится в одном месте в базовом классе, что позволяет править её только в одном месте сразу для всех классов писем. Добавление же новых типов писем заключается в создании наследника и определении в нем данных которые нужно извлечь и которые нужно подставить в шаблон тела письма, что уменьшает вероятность ошибки при составлении тела запроса к сервису и при надлежащем освоении реализации может упростить и создание новых писем для не знакомого с апи сервиса рассылок пользователя.

Комментарии (55)


  1. mozg3000tm Автор
    15.09.2022 07:45

    Я в заголовке написал про отправку писем, и как то дальше это стало пониматься как отправка писем через почту по smpt, но по факту это http запросы к rest api сервиса рассылок, и уже он отправляет сома письмо.

    Здесь мне подсказали что письма (те что е-почта) можно отправлять так:

    <?php
    $email = (new Email())
                ->from('hello@example.com')
                ->to('you@example.com')
                ->subject('Time for Symfony Mailer!')
                ->text('Sending emails is fun again!')
                ->html('<p>See Twig integration for better HTML integration!</p>');
    
            $mailer->send($email);

    И сейчас я подумал, что для http запросов уже есть аналогичный Email класс Request и мой класс Message следует наследовать от него, и тогда можно будет написать следующий код, который будет работать и с классом Email и c классом NewReserveEmail (который не е-почта).

    <?php
    
    namespace app\services\backend\email;
    
    use app\interfaces\MessageInterface;
    use app\interfaces\SenderInterface;
    use yii\httpclient\Client;
    
    class Sender implements SenderInterface
    {
        private Client $client;
    
        public function __construct(Client $client)
        {
    
            $this->client = $client;
        }
    
        public function send(MessageInterface $message)
        {
            return $this->client->send($message);
        }
    }

    Подробнее напишу в следующей статье...

    PS. Кстати yii\httpclient\Request имеет метод send() и сам себя отправляет, через yii\httpclient\Client внутри себя. Хотя конечно это было написано давно...


  1. lair
    13.09.2022 22:56
    +6

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

    То ли я чего-то не понимаю в вашем подходе (он выглядит тривиальным), то ли у вас какие-то странные источники, если в них нет этого тривиального подхода.


    С другой стороны, впрочем, использование для отправки письма метода Send на самом объекте письма — это плохая практика, потому что ваш класс намертво прибит к одному сервису, а ваши клиенты — к одному классу. Не надо так.


    Логика составления письма и логика его отправки — это два разных консерна, и они должны быть в несвязанных классах.


    1. funca
      14.09.2022 00:01
      -4

      это плохая практика, потому что ваш класс намертво прибит к одному сервису, а ваши клиенты — к одному классу.

      ООП не запрещает инжектать сервисы в модели - в нем вообще нет таких слов. Если клиенты прибились к конкретному классу, вместо интерфейса, то это проблема клиентов).

      То чем вы недовольны это вопрос выбранной архитектуры. Вообще говоря архитектура зависит от требований и если нет противоречий, то все хорошо и код имеет право на жизнь. Автор использует Yii, который даёт некоторые подсказки, но здесь мне кажется это не существенно.

      С точки зрения некоторых архитектур инжектать сервисы в модели это моветон. Но это вероятно тема отдельной статьи, типа https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/.


      1. lair
        14.09.2022 00:49
        +4

        ООП не запрещает инжектать сервисы в модели — в нем вообще нет таких слов

        Я, вроде бы, ничего не говорил про инжект чего-то куда-то.


        То чем вы недовольны это вопрос выбранной архитектуры.

        Вот я и указываю на недостатки выбранной архитектуры.


        1. funca
          14.09.2022 08:52

          Тогда начните с https://www.yiiframework.com/doc/guide/2.0/en/concept-di-container. Любые оценочные суждения справедливы только в контексте. Автор писал статью про ООП, а не какую-то конкретную архитектуру или требования. Не вижу повода придираться.


          1. bombe
            14.09.2022 09:41

            Начните с того, что термин "Модель" (используемый в статье, и Вами в предыдущем комментарии) с точки зрения любой архитектуры - это не про объект письма, который больше похож на ValueObject. И ООП - это не про то, что данный объект письма может сам себя выслать (чего?) и с помощью DI втянуть в себя зависимости (?!). Автор может и про ООП, но конкретно своё видение, как "ООП - это когда объект с проперти и методами. А еще можно расширить клас. И наследовать, ага.". Инверсия зависимостей, единственная ответственность, паттерны - не, не слышал.


            1. mozg3000tm Автор
              14.09.2022 11:00

              Термин "Модель" впервые употреблён вами самими.
              Вы правы относительно паттерном, инверсий зависомостей, единственная ответственность и прочее, и если приглядитесь, то кое что даже найдёте. Например, туже инверсию зависимостей пояснение тут, шаблонный метод (паттерн) и др. И вообще 4 (расширенных) принципа ООП это - абстракция, инкапсуляция, наследование и полиморфизм и так или иначе это мной использовалось либо предполагается что можно использовать с некоторыми изменениями.

              Но вы немного не с тем сравниваете, вы сравниваете с некой глобальной теорией ОПП, а я говорю о практической задаче, которую на сколько мне известно в большинстве случаев реализуют через прямую отправку данных с помощью класса mailer ( то и просто через curl), максимум обёрнутую в какой-то сервис, в методах которого происходит таже прямая отправка. Я же применил объектно ориентированных подход и сделал объект письма и описал какое преимущество это имеет для пользователя этого класса.


              1. bombe
                14.09.2022 13:33
                +3

                Согласен, в статье просто часто мелькает неймспейс models, и то, что абстрактный класс сообщения - расширенная Model. Именно термин не упоминается, разве что вскользь.

                По поводу преимуществ. Все преимущества сойдут на нет, когда Ваш god-object надо будет изменять. К примеру, перед отправкой сообщения (или после) нужна какая-то логика. А потом надо будет добавить дополнительный аргумент в конструктор. И т.д.

                Сообщение ничего не должно знать, каким образом оно будет отправляться. И будет ли отправляться вообще. И тем более не должно отправлять себя само, содержа в себе инстанс реализации отправки сообщений, для этих целей лучше создать сервис, который будет заниматься отправкой ЛЮБОГО сообщения. Он ничего не должен знать о том, каким образом формируется отправляемое сообщение. Его задача - просто отправить. У него должен быть метод отправки с аргументом - интерфейс сообщения. Именно интерфейс, а не конкретная реализация.

                Но Вы вправе делать, как Вам угодно. Если оно работает - то почему бы и нет? =)


                1. mozg3000tm Автор
                  14.09.2022 15:38

                  Прямо таки классическая моральная проблема)))
                  вы оцениваете человека за грехи которых он ещё не совершил из каких-то абстрактных соображений. Типа через год я совершу очень плохой поступок, поэтому я должен умереть сейчас же.

                  Я не претендовал на решение вопроса всего и вся. тем более что ответ уже известен. 42.


          1. lair
            14.09.2022 13:47
            +2

            Тогда начните с https://www.yiiframework.com/doc/guide/2.0/en/concept-di-container.

            … ничего не поменялось. В том числе и потому, что в посте нигде не используется dependency injection.


            Автор писал статью про ООП

            Вот именно с точки зрения ООП я и считаю, что декомпозиция предметной области сделана неправильно.


            1. funca
              14.09.2022 23:18

              В том числе и потому, что в посте нигде не используется dependency injection.

              Вы ее вообще читали, или как я - сначала комменты?)

              /**
              * Constructor
              */
              public function __construct(bool $useTemplate)
              {
                parent::__construct();
              
                $this->useTemplate = $useTemplate;
              
                try {
                    $this->mailer = Yii::$container->get(EmailServiceInterface::class);

              если бы в модель не инжектился сервис для отправки почты - ну здесь через контейнер, который используется как сервис-локатор, - как бы она вообще могла что-то отправить.


              1. lair
                14.09.2022 23:23

                Уже неоднократно обсуждено в комментариях: это не dependency injection, это service locator, совершенно другой паттерн.


                если бы в модель не инжектился сервис для отправки почты — ну здесь через контейнер, который используется как сервис-локатор

                "Как сервис-локатор" — это не "в модель инжектится", это "модель забирает". Принципиально разные вещи.


      1. mozg3000tm Автор
        14.09.2022 11:48
        -2

        Я бы плюсанул ваш комментарий, но карму обнулили)))

        Действительно комментатор пропустил момент в пробросом зависимости, который я пояснил в этом ответе.

        С точки зрения некоторых архитектур инжектать сервисы в модели это моветон.

        Статья на английском и довольно длинная, пока не прочитал, поэтому отвечу без её учёта.
        Я применил данный подход потому, что он проще в реализации. Чтобы избежать проброса зависимости в модель, следовало сделать сервис в который бы приходила бы данная модель и сервис отправки писем, там вызывать у модели методы prepareData composeMessage и передавать готовый результат в сервис отправки писем в котором уже и вызывать метод sendMessage. Тогда затруднения описанные коллегами были бы устранены. И я такой подход считаю более правильным в целом, но в конкретной ситуации я делал определённую работу в определённом контексте, поэтому реализовал как проще как смог в тех конкретных условия в которых я даже не был знаком с сервисом unione нужно было разбираться с другими вещами.


        1. lair
          14.09.2022 15:09
          +1

          И я такой подход считаю более правильным в целом, но в конкретной ситуации я делал определённую работу в определённом контексте, поэтому реализовал как ~~проще ~~ как смог в тех конкретных условия в которых я даже не был знаком с сервисом unione нужно было разбираться с другими вещами.

          Так зачем же вы публикуете код, который "менее правильный"? Понятно, почему для решения конкретной задачи где-то в проде иногда надо поступиться принципами в пользу времени реализации, но что мешало для статьи переделать?


          1. mozg3000tm Автор
            14.09.2022 16:08
            +1

            Если мне сникерс нравиться большее чем марс (сникерс более правильный чем марс очевидно), то это не значит что я откажусь от марса, если мне предложат марс, а не сникерс.
            Does it make a sence?


            1. lair
              14.09.2022 16:08
              +1

              Речь идет не о том, что вам нравится, а о том, что более правильно.


      1. mozg3000tm Автор
        14.09.2022 13:13

        из приведённой вами статьи:

        In other words, our Driving Adapters are Controllers or Console Commands who are injected in their constructor with some object whose class implements the interface (Port) that the controller or console command requires.

        In a more concrete example, a Port can be a Service interface or a Repository interface that a controller requires. The concrete implementation of the Service, Repository or Query is then injected and used in the Controller.

        Если сопоставить

        Controller - в Message, а

        Service interface - в EmailServiceInterface, то

        получается у меня всё по феншую)))

        Вы можете сказать, что то КОНТРОЛЛЕР, а у тебя MODEL, а это совсем другое, но я отвечу, что разница между контроллером и моделью лишь функциональная (в контексте MVC), но то и другое это объекты в которые что-то пробрасывается, и потом вызывается его метод. Если критики это не понимают, то значит эта статья попала в точку и я рад что я описал этот конкретный случай.


        1. bombe
          14.09.2022 14:52
          +1

          но я отвечу, что разница между контроллером и моделью лишь функциональная (в контексте MVC)

          да что Вы не говорите? С точки зрения MVC, модель - это бизнес логика, а в контроллер инжектятся зависимости, обрабатываются входящие параметры и с помощью команд модели ей передается управление. С точки зрения чистой архитектуры бизнес логика должна быть изолирована от инфраструктурного слоя, который Вы предлагаете пробросить в модель. Думаю, критики прекрасно понимают, что именно Вы пытаетесь сделать.


          1. mozg3000tm Автор
            14.09.2022 15:59

            С точки зрения MVC, модель - это бизнес логика, а в контроллер инжектятся зависимости

            Вы лишь повторили, то что я сказал, разница между ними функциональная - т.е. у них разные функции.

            в контроллер инжектятся зависимости

            и в модели пробрасываются зависимости вы могли добавить)))

            Я то говорил с т.з языка. И то и то это инстансы классов, в конструктор которых можно передать переменные - и это можно назвать инъекций зависимостей.
            Но вы ограничиваете её только конроллером, хотя в других языках например инъекция осуществляется на стадии создания приложения, например что то типо такого:

            app = new Server(config)->use(MySQLProvider)->use(RabbitMq)

            В широком смысле под инъекцией понимается проброс внутрь объекта нужных ему переменных (классов).


    1. mozg3000tm Автор
      14.09.2022 10:44

      Позвольте мне дополнительно пояснить вам некоторые моменты о которых я написал.

      Действительно, наверное было бы понятнее если бы я написал: "нам не встречалось ещё в доступных источниках такого подхода К ОТПРАВКЕ ПИСЕМ". Но похоже основная часть вашего комментария подразумевает что так делать не следует поэтому сразу повторюсь в чём плюс использования моего класса в разработке для пользователя этого класса.

      объявление же новых типов писем заключается в создании наследника и определении в нем данных которые нужно извлечь и которые нужно подставить в шаблон тела письма, [...] и при надлежащем освоении реализации может упростить и СОЗДАНИЕ НОВЫХ (ТИПОВ - прим. автора) ПИСЕМ ДЛЯ НЕ ЗНАКОМОГО С АПИ СЕРВИСА РАССЫЛОК ПОЛЬЗОВАТЕЛЯ.

      По поводу вашего комментария:

      ваш класс намертво прибит к одному сервису, а ваши клиенты — к одному классу. 

      Как верно заметили коллеги, у меня нет привязки к одному классу и одному сервису, потому что я использую контейнер зависимостей для создания нужного сервиса отправки письма, и именно поэтому мой код с небольшими правками можно перенести в любой другой фреймворк например, ведь именно это подразумевается под отвязкой от конкретного сервиса. Если отвязаться от самого сервиса рассылок, вообще код перестанет быть валидным и я не ставил задачей построить архитектуру которая будет отправлять что угодно куда угодно. Хотя я стремлюсь к этому и хотел бы что-то такое реализовать в практической области.

      Так вот под капотом для http запросов используется клиент Yii2 HttpClient\Client, но к нему нет привязки потому что он помещён внутрь UniOneService который реализует интерфейс EmailServiceInterface. у меня в коде это выглядит так:

      <?php
      public function __construct(bool $useTemplate)
          {
              [...]
                  $this->mailer = Yii::$container->get(EmailServiceInterface::class);
              [...]
          }

      Соответственно в контейнере зависимостей прописано следующее:

      <?php
      ClientInterface::class => HttpClient::class,
      EmailServiceInterface::class => function ($container) {
      	$client =$container->get(ClientInterface::class);
      	return new UniOneService($client);
      },

      Это Yii::$container->get(EmailServiceInterface::class) наверное выглядит непривычно, но это аналогично как если бы использовалось автосвязывание и было написано так:

      <?php
      public function __construct(EmailServiceInterface $mailerService)
      {
      		$this->mailer = $mailerService;
      }

      Но меня не научили как настроить юишный контейнер чтобы он автосвязывал произвольные классы, поэтому у меня получение объекта из контейнера происходит руками.

      Надеюсь это прояснило для вас упомянутые моменты. И вы согласны что пользователю (программист), которому нужно создать новый тип писем и который не знаком с апи сервиса, будет легче это сделать при данном подходе.


      1. lair
        14.09.2022 13:58
        +2

        Действительно, наверное было бы понятнее если бы я написал: "нам не встречалось ещё в доступных источниках такого подхода К ОТПРАВКЕ ПИСЕМ".

        Какого такого? Все еще не понятно.


        СОЗДАНИЕ НОВЫХ (ТИПОВ — прим. автора) ПИСЕМ ДЛЯ НЕ ЗНАКОМОГО С АПИ СЕРВИСА РАССЫЛОК ПОЛЬЗОВАТЕЛЯ.

        Понимаете ли в чем дело… эта формулировка уже говорит о правильной декомпозиции. Создание новых типов писем не должно никак зависеть от апи сервиса рассылок, поэтому письма просто не должны никак зависеть от апи сервиса рассылок.


        Если вы на мгновение задумаетесь о реальной жизни (хотя аналогии всегда врут), то обнаружите, что письмо никак не зависит от того, бросите вы его в почтовый ящик, или ногами отнесете на почту, или вызовете курьера на дом. Оно даже не зависит от конкретной почтовой компании, потому что оно от него изолировано через адаптер в виде конверта (которые у компаний разные).


        у меня нет привязки к одному классу и одному сервису, потому что я использую контейнер зависимостей для создания нужного сервиса отправки письма, и именно поэтому мой код с небольшими правками можно перенести в любой другой фреймворк например, ведь именно это подразумевается под отвязкой от конкретного сервиса

        Во-первых, под отвязкой от конкретного сервиса подразумевается не перенос в другой фреймворк, а использование другого сервиса отправки почты. А во-вторых, если вы утверждаете, что у вас нет привязки, давайте рассмотрим три следующих сценария:


        1. вам надо написать юнит-тесты на ваш класс Message
        2. вам надо заменить unione на Exchange или SendGrid
        3. вам надо в рамках одного и того же приложения использовать одновременно несколько разных почтовых сервисов (например, письма разным получателям должны уходить от разных отправителей, или письма клиентам должны идти через SendGrid, а внутренним сотрудникам — через Exchange)

        Может ли ваше решение поддержать это без изменения кода? Если не может, то сколько кода надо поменять?


        Это Yii::$container->get(EmailServiceInterface::class)

        Ничего непривычного в этом нет, это обычный service locator.


        но это аналогично как если бы использовалось автосвязывание и было написано так:

        Нет, это не "аналогично". Это разница между service locator и dependency injection, двумя принципиально разными паттернами.


        И вы согласны что пользователю (программист), которому нужно создать новый тип писем и который не знаком с апи сервиса, будет легче это сделать при данном подходе.

        Нет, не согласен. Будет легче по сравнению с чем?


        1. mozg3000tm Автор
          14.09.2022 15:32
          -2

          Вы занимаетесь софистикой, говорите умные вещи, которые сами по себе истино верны, но замкнуты сами в себе и не имеют практических выводов.

          Может ли ваше решение поддержать это без изменения кода? Если не может, то сколько кода надо поменять?

          Если я каким-то образом дал понять, что я предлагаю что-то подобное, то извините, не хотел вас ввести в заблуждение. Статья называется - "Использование ООП подхода для рассылки писем через Unione (php, Yii2)".

          1. вам надо заменить unione на Exchange или SendGrid

          Тем не менее, если бы вы не занимались софистикой, то вы бы обратили внимание на следующее, для того чтобы использовать другой сервис, нужно сделать всего лишь два изменения и всё будет работать прекрасно мой оппонирующий друг, а именно
          1е - изменить метод Message::composeMessage
          2е - изменить сопоставление EmailServiceInterface => UniOneService на новый сервис

          и это всё!

          вам надо в рамках одного и того же приложения использовать одновременно несколько разных почтовых сервисов

          Это тоже можно организовать, сделав несколько наследников с по разному переопределёнными методами composeMessage и прокидывая разные сервисники в EmailServiceInterface (чтобы вообще ничего не менять больше можно в рантайме менять сопоставление).

          Нет, это не "аналогично". Это разница между service locator и dependency injection, двумя принципиально разными паттернами.

          это опять софистика. вот вам инъекция вместо локатора (надеюсь вы не отождествляете dependency injection и autowiring)

          <?php
          public function __construct(string $service)
          {
              [...]
                  $this->mailer = Yii::$container->get($service);
              [...]
          }

          Но тут руками нужно подставлять в конструктор $service.

          Создание новых типов писем не должно никак зависеть от апи сервиса рассылок, поэтому письма просто не должны никак зависеть от апи сервиса рассылок.

          Звучит конечно умно, но он не может не зависить от апи хотя бы тому что содержит данные для запроса к этому апи. Но для целей софистики можно бесконечно использовать не достаточную определённость слов что бы говорить бесконечно ни о чём. Я то говорил про практическую ценность того что чтобы создать новый тип писем нужно лишь определить два метода, и для этого совсем не обязательно знать вообще апи тому кто из переопределяет.

          Будет легче по сравнению с чем?

          По сравнению с тем, что пользователю вместо переопределения двух методов нужно полностью составлять запрос к апи, что требует хоть какого то его знания.

          Кстати вот он (из документации)

          require 'vendor/autoload.php';
          
          $headers = array(
              'Content-Type' => 'application/json',
              'Accept' => 'application/json',
              'X-API-KEY' => 'API_KEY',
          );
          
          $client = new \GuzzleHttp\Client([
              'base_uri' => 'https://eu1.unione.io/ru/transactional/api/v1/'
          ]);
          
          $requestBody = [
            "message" => [
              "recipients" => [
                [
                  "email" => "user@example.com",
                  "substitutions" => [
                    "CustomerId" => 12452,
                    "to_name" => "John Smith"
                  ],
                  "metadata" => [
                    "campaign_id" => "email61324",
                    "customer_hash" => "b253ac7"
                  ]
                ]
              ],
              "template_id" => "string",
              "skip_unsubscribe" => 0,
              "global_language" => "string",
              "template_engine" => "simple",
              "global_substitutions" => [
                "property1" => "string",
                "property2" => "string"
              ],
              "global_metadata" => [
                "property1" => "string",
                "property2" => "string"
              ],
              "body" => [
                "html" => "<b>Hello, {{to_name}}</b>",
                "plaintext" => "Hello, {{to_name}}",
                "amp" => "<!doctype html><html amp4email><head> <meta charset=\"utf-8\"><script async src=\"https://cdn.ampproject.org/v0.js\"></script> <style amp4email-boilerplate>body[visibility:hidden]</style></head><body> Hello, AMP4EMAIL world.</body></html>"
              ],
              "subject" => "string",
              "from_email" => "user@example.com",
              "from_name" => "John Smith",
              "reply_to" => "user@example.com",
              "track_links" => 0,
              "track_read" => 0,
              "headers" => [
                "X-MyHeader" => "some data",
                "List-Unsubscribe" => "<mailto: unsubscribe@example.com?subject=unsubscribe>, <http://www.example.com/unsubscribe/{{CustomerId}}>"
              ],
              "attachments" => [
                [
                  "type" => "text/plain",
                  "name" => "readme.txt",
                  "content" => "SGVsbG8sIHdvcmxkIQ=="
                ]
              ],
              "inline_attachments" => [
                [
                  "type" => "image/gif",
                  "name" => "IMAGECID1",
                  "content" => "R0lGODdhAwADAIABAP+rAP///ywAAAAAAwADAAACBIQRBwUAOw=="
                ]
              ],
              "options" => [
                "send_at" => "2021-11-19 10:00:00",
                "unsubscribe_url" => "https://example.org/unsubscribe/{{CustomerId}}",
                "custom_backend_id" => 0,
                "smtp_pool_id" => "string"
              ]
            ]
          ];
          
          try {
              $response = $client->request('POST','email/send.json', array(
                  'headers' => $headers,
                  'json' => $requestBody,
                 )
              );
              print_r($response->getBody()->getContents());
           }
           catch (\GuzzleHttp\Exception\BadResponseException $e) {
              // handle exception or api errors.
              print_r($e->getMessage());
           }


          1. lair
            14.09.2022 15:46
            +1

            Если я каким-то образом дал понять, что я предлагаю что-то подобное, то извините, не хотел вас ввести в заблуждение.

            Да, вы написали: "у меня нет привязки к одному классу и одному сервису". А она, как выясняется, все-таки есть.


            И это — жесткую привязку — я считаю плохим дизайном.


            нужно сделать всего лишь два изменения и всё будет работать прекрасно мой оппонирующий друг, а именно
            1е — изменить метод Message::composeMessage
            2е — изменить сопоставление EmailServiceInterface => UniOneService на новый сервис

            Это как раз подтверждает то, о чем я говорю: ваш Message жестко привязан к сервису отправки. В более правильном дизайне потребовалось бы одно изменение (в composition root).


            А еще, кстати, вы ошибаетесь в числе изменений. Посмотрите, в каком количестве мест в коде у вас употребляется слово unione — это и будет то минимальное количество мест, которые понадобится поменять. А потом добавьте туда тот факт, что вообще-то не все провайдеры поддерживают ваши substitutions.


            Это тоже можно организовать, сделав несколько наследников с по разному переопределёнными методами composeMessage

            … и как вы будете решать проблему того, что у вас есть наследники на типы писем, и наследники на сервисы отправки?


            это опять софистика. вот вам инъекция вместо локатора (надеюсь вы не отождествляете dependency injection и autowiring)

            Нет, это не инъекция (в терминах паттерна dependency injection). Я не знаю, что такое autowiring (опять же, в терминах паттернов, а не конкретного фреймворка).


            Звучит конечно умно, но он не может не зависить от апи хотя бы тому что содержит данные для запроса к этому апи.

            Подождите, а как же… dependency inversion? Если типы писем зависят от абстракции "письмо", и при этом сервис отправки зависит от абстракции "письмо", то типы писем больше не зависят от сервиса отправки, только от общей абстракции.


            По сравнению с тем, что пользователю вместо переопределения двух методов нужно полностью составлять запрос к апи, что требует хоть какого то его знания.

            А зачем вы сравниваете с заведомо плохим дизайном? Сравнивать надо, например, с дизайном, где письмо и сервис отправки разделены, и пользователь просто переопределяет методы создания письма, а потом передает это письмо в сервис отправки.


            1. mozg3000tm Автор
              14.09.2022 16:16
              -1

              Нет, это не инъекция (в терминах паттерна dependency injection). Я не знаю, что такое autowiring (опять же, в терминах паттернов, а не конкретного фреймворка).

              Обновите своё определение.

              In software engineeringdependency injection is a design pattern in which an object or function receives other objects or functions that it depends on. A form of inversion of control, dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs.[1][2][3] The pattern ensures that an object or function which wants to use a given service should not have to know how to construct those services. Instead, the receiving 'client' (object or function) is provided with its dependencies by external code (an 'injector'), which it is not aware of.

              Согласно ему, то что я написал это и есть DI.

              <?php
              public function __construct(string $service)
              {
                  [...]
                      $this->mailer = Yii::$container->get($service);
                  [...]
              }
              

              Объект создаётся контейнером зависимостей и класс не знает ничего о том как его создавать. Это лишь одна из корявых реализаций инъекции зависимостей, которая не похожа на то что вам привычно.


              autowiring - это когда контейнер зависимостей сам создаёт объекты просто по факту их наличия в определении метода, так, что не нужно сервис локатор использовать.


              1. lair
                14.09.2022 16:20

                Согласно ему, то что я написал это и есть DI.

                Нет, вы же не передаете зависимость. Вы передаете какую-то строчку.


                Объект создаётся контейнером зависимостей и класс не знает ничего о том как его создавать. Это лишь одна из корявых реализаций инъекции зависимостей...

                Класс знает, что ему надо пойти в контейнер. Это сервис-локатор, а не инъекция.


              1. BoShurik
                14.09.2022 16:27
                +1

                Там дальше по тексту отдельно упоминается service locator

                Because the client does not build or find the service itself, it typically only needs to declare the interfaces of the services it uses, rather than their concrete implementations.


                1. mozg3000tm Автор
                  14.09.2022 17:32

                  сервис локатор тут не упоминается, на сколько I speek English.

                  ключевая фраза тут - "client does not build or find the service itself"

                  и это выполняется, класс Message не создаёт и не ищет сервис сам.

                  <?php
                  public function __construct(string $service)
                  {
                      [...]
                          $this->mailer = Yii::$container->get($service);
                      [...]
                  }


                  1. BoShurik
                    14.09.2022 17:39

                    Ссылка ведет на статью про сервис локатор (который собственно и создан для того чтобы find the service) + он таки ищет этот сервис с помощью Yii::$container. Это ли не оно?

                    Ну и вы просто вдумайтесь, какого будет потом дебажить такой код. Ни намека на то, чем является $this->mailer


                    1. mozg3000tm Автор
                      14.09.2022 17:52

                      Ну и вы просто вдумайтесь, какого будет потом дебажить такой код. Ни намека на то, чем является $this->mailer

                      Вы не заметили этот намёк.

                      <?php
                          /**
                           * @var UniOneService
                           */
                          private $mailer;

                      Плюс это ничем не отличается от такого

                      <?php
                      public function __construct(EmailServiceInterface $mailerService)
                      {
                      		$this->mailer = $mailerService;
                      }

                      Ссылка ведет на статью про сервис локатор (который собственно и создан для того чтобы find the service) + он таки ищет этот сервис с помощью Yii::$container. Это ли не оно?

                      Ссылка может просто ведёт на ключевые слова find service, из это логически не следует что клиент сам ищет класс использую сервис локатор.


                  1. lair
                    14.09.2022 17:54
                    +1

                    сервис локатор тут не упоминается, на сколько I speek English.

                    Вы, похоже, не очень speak English. Find и Locate — синонимы.


                    класс Message не создаёт и не ищет сервис сам.

                    $container->get — это типовая реализация сервис-локатора, потому что потребитель сам запрашивает у контейнера (о котором он должен знать) реализацию сервиса. Контейнер выступает локатором.


                    (если бы вы попробовали ответить на мой вопрос про юнит-тесты, вы бы достаточно быстро поняли, почему это различие важно)


            1. mozg3000tm Автор
              14.09.2022 16:30

              нужно сделать всего лишь два изменения и всё будет работать прекрасно мой оппонирующий друг, а именно1е — изменить метод Message::composeMessage2е — изменить сопоставление EmailServiceInterface => UniOneService на новый сервис

              Это как раз подтверждает то, о чем я говорю: ваш Message жестко привязан к сервису отправки. В более правильном дизайне потребовалось бы одно изменение (в composition root).

              А еще, кстати, вы ошибаетесь в числе изменений. Посмотрите, в каком количестве мест в коде у вас употребляется слово unione — это и будет то минимальное количество мест, которые понадобится поменять. А потом добавьте туда тот факт, что вообще-то не все провайдеры поддерживают ваши substitutions.

              Одна из причин написания статьи это community sharing, получение обратной связи и прочие наставления. Это довольно дельные замечания, и именно их я хотел бы получиться в самом начале)). Я люблю когда подсказывают по существу, иначе смысл обмениваться комментариями. Спасибо.

              По поводу

              Это как раз подтверждает то, о чем я говорю: ваш Message жестко привязан к сервису отправки. В более правильном дизайне потребовалось бы одно изменение (в composition root).

              Я наконец-то понял о чём вы речь ведёте. Вы хотите чтобы весь уже написанный код с конкретными типами писем продолжил работать при смене почтового провайдера (сервиса). Да, такое пожалуй не достижимо одним изменением.

              Я хотел осветить именно подход к письму как объекту, и статья больше про это. Надеюсь вы это сможете принять.


              1. lair
                14.09.2022 16:35

                Я хотел осветить именно подход к письму как объекту, и статья больше про это.

                А этот подход — тривиален (и мне удивительно, что вы не нашли его "в источниках"): System.Net.Mail.MailMessage, SendGrid, SNS.


                1. mozg3000tm Автор
                  14.09.2022 16:50

                  А этот подход — тривиален (и мне удивительно, что вы не нашли его "в источниках")

                  System.Net.Mail.MailMessage

                  Я говорил про php источники.
                  С чего бы я стал гуглить C#?
                  В php немного иначе кодырят, и если бы видели этот код, то вы бы поняли о чём я)))

                  <?php
                      $to      = 'nobody@example.com';
                      $subject = 'the subject';
                      $message = 'hello';
                      $headers = 'From: webmaster@example.com'       . "\r\n" .
                                   'Reply-To: webmaster@example.com' . "\r\n" .
                                   'X-Mailer: PHP/' . phpversion();
                  
                      mail($to, $subject, $message, $headers);
                  ?>


                  1. BoShurik
                    14.09.2022 16:51

                    Вот на php: symfony/mailer


                    1. lair
                      14.09.2022 16:56

                      … и сделано ровно с описанным выше разделением на письмо и мейлер.


                  1. lair
                    14.09.2022 16:53
                    +1

                    Я говорил про php источники.

                    А я говорю про ООП в общем. Это, все-таки, общий подход, не изолированный для каждого языка.


                    В php немного иначе кодырят, и если бы видели этот код, то вы бы поняли о чём я

                    Я как раз стараюсь думать, что люди могут хорошо писать на любом языке, и не надо поощрять предрассудки вида "если php, то говнокод". Поэтому я предполагаю, что разработчик знаком с общими практиками.


                    Но вот вам то же самое конкретно для PHP: SendGrid (и для SNS будет аналогично, потому что SNS генерит врапперы для всех языков аналогично).


                    1. mozg3000tm Автор
                      14.09.2022 17:16

                      Я как раз стараюсь думать, что люди могут хорошо писать на любом языке, и не надо поощрять предрассудки вида "если php, то говнокод". Поэтому я предполагаю, что разработчик знаком с общими практиками.

                      Слова достойные великого мужа.

                      Посмотрев на код отсюда и то что вы привели и если оставить момент где вызывается метод send, вы можете признать, что момент с использованием базового класса и формирование тела запроса через шаблонный метод и определение в наследнике только подстановок это ООП подход, он сделан мой и отличается от тех что привели вы и @BoShurik?

                      у для пример там это

                      <?php
                      $email = (new Email())
                                  ->from('hello@example.com')
                                  ->to('you@example.com')
                                  //->cc('cc@example.com')
                                  //->bcc('bcc@example.com')
                                  //->replyTo('fabien@example.com')
                                  //->priority(Email::PRIORITY_HIGH)
                                  ->subject('Time for Symfony Mailer!')
                                  ->text('Sending emails is fun again!')
                                  ->html('<p>See Twig integration for better HTML integration!</p>');
                      
                              $mailer->send($email);

                      и если бы я отрефакторил и у меня бы вызывалось так

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


                      1. lair
                        14.09.2022 17:52

                        вы можете признать, что момент с использованием базового класса и формирование тела запроса через шаблонный метод и определение в наследнике только подстановок это ООП подход, он сделан мой

                        Я никогда и не спорил, что это ОО-подход. Я просто считаю, что он настолько тривиален, что там особо нечего обсуждать.


                        (Вот пообсуждать, надо делать так или через фабрику — интересно. Но такого варианта у вас нигде не рассматривается.)


                      1. mozg3000tm Автор
                        14.09.2022 18:06

                        Боюсь моя карма и так сильно пострадала от всех этих обсуждений и ещё разговора о фабрике не переживёт :)


  1. mozg3000tm Автор
    15.09.2022 07:45

    Я в заголовке написал про отправку писем, и как то дальше это стало пониматься как отправка писем через почту по smpt, но по факту это http запросы к rest api сервиса рассылок, и уже он отправляет сома письмо.

    Здесь мне подсказали что письма (те что е-почта) можно отправлять так:

    <?php
    $email = (new Email())
                ->from('hello@example.com')
                ->to('you@example.com')
                ->subject('Time for Symfony Mailer!')
                ->text('Sending emails is fun again!')
                ->html('<p>See Twig integration for better HTML integration!</p>');
    
            $mailer->send($email);

    И сейчас я подумал, что для http запросов уже есть аналогичный Email класс Request и мой класс Message следует наследовать от него, и тогда можно будет написать следующий код, который будет работать и с классом Email и c классом NewReserveEmail (который не е-почта).

    <?php
    
    namespace app\services\backend\email;
    
    use app\interfaces\MessageInterface;
    use app\interfaces\SenderInterface;
    use yii\httpclient\Client;
    
    class Sender implements SenderInterface
    {
        private Client $client;
    
        public function __construct(Client $client)
        {
    
            $this->client = $client;
        }
    
        public function send(MessageInterface $message)
        {
            return $this->client->send($message);
        }
    }

    Подробнее напишу в следующей статье...

    PS. Кстати yii\httpclient\Request имеет метод send() и сам себя отправляет, через yii\httpclient\Client внутри себя. Хотя конечно это было написано давно...


    1. bombe
      15.09.2022 12:48

      Да ладно?! Разве не это Вам пытались вчера вталдычить, а Вы упорно продолжали спорить, что изобрели что-то новое и удобное. А оказывается, нужен сервис отправки сообщений. Эвоно как, неожиданно =)


      1. mozg3000tm Автор
        15.09.2022 13:21
        -1

        Друг мой, суть моей статьи была не в том КАК отправлять, а в использовании наследования и шаблонного метода для порождения новых писем. Изменение реализации отправки не повлияло на эту фишку совсем, просто метод send вызывается в другом месте. Поэтому вы мне вталдычели лишь то, что чем меньше отвечаешь на комментарии тем меньше тебе насуют -1 в рейтинг.


        1. bombe
          15.09.2022 13:46

          Ну, во-первых, раз уж на то пошло, то Вы мне не друг =) А во-вторых, лично я Вам минусов не ставил, делать мне нечего =)

          Но Вы и дальше продолжаете. Хорошо, удачи в познании разных архитектур приложений =) Моё мнение (не должно являться правильным для всех) - Вы совсем не понимаете, зачем Вам ООП.


          1. mozg3000tm Автор
            15.09.2022 14:40
            -1

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

            Некоторые вещи вообще не свойство ООП как такового и применимо к другим стилям программирования. (прежде чем минусануть пост прошу учесть что у меня есть ссылка на источник вот "Семь раз отмерь, а SOLID все равно не про ООП. Монолог об архитектуре", а в ней есть ещё ссылка на статью, которую стоит тоже прочитать "Размышления о принципах проектирования"). А некоторые вещи как Паттерны, это лишь техники, а не само ООП.

            Вы совсем не понимаете, зачем Вам ООП.

            На этот счёт я думаю надо сперва освоить основы о который я написал выше, а потом уже лезть в дебри.


            1. bombe
              15.09.2022 16:42

              Вы предлагаете мне освоить основы из статьи на хабре? Спасибо, занятно =)

              Если серьезно, я прочитал не одну книгу Фаулера, Эванса, Вон Вернона и многих других. И минусую Вас не я, хватит на этом заострять внимание.


              1. mozg3000tm Автор
                15.09.2022 17:49

                Вы предлагаете мне освоить основы из статьи на хабре? Спасибо, занятно =)

                Вообще то, вроде это вы мне сказали что я не понимаю зачем мне ООП, а я лишь в связи с этим обозначил свои приоритеты в его изучении подкрепив их ссылками.

                И минусую Вас не я, хватит на этом заострять внимание.

                Я и обращаюсь к тому кто минусует, а не к вам, пусть и в сообщении к вам. Хотя это больше стёб конечно...

                Если серьезно, я прочитал не одну книгу Фаулера, Эванса, Вон Вернона и многих других.

                Есть библия, а есть толкователи, читать толкователей не менее полезно чем библию.
                Есть гражданский кодекс, а есть комментарии к гражданскому кодексу, читать их не менее полезно чем ГК.


                1. lair
                  15.09.2022 20:29

                  я лишь в связи с этим обозначил свои приоритеты в его изучении

                  Может быть, стоит тогда сначала изучить, а потом писать статьи на хабре?


                1. bombe
                  16.09.2022 08:25
                  +1

                  это вы мне сказали что я не понимаю зачем мне ООП

                  Да, я и дальше так считаю. Вы взяли инструмент, но не знаете как ним пользоваться. Но уже считаете, что статьями на хабре можете учить других. Повторю еще раз - то что Вы "изобрели" в статье - полнейший бред, Вы не понимаете зачем Вам ООП. ООП это не только про объекты с методами и классами, хоть и объект является ключевым понятием.

                  Тем не менее Вы считаете, что основы надо читать мне. Хорошо, я не спорю, спасибо =) Я хоть и читал уже множество раз в разных источниках, но уверен, что в любом случае прочитаю как-то еще раз. Учиться и обновлять знания, знаете ли, приходится постоянно.


                  1. mozg3000tm Автор
                    16.09.2022 11:20

                    то что Вы "изобрели" в статье - полнейший бред,

                    Что я изобрёл в статье?

                    Я ничего не изобретал в статье. Заголовок и первый абзац являются завлекательными, и пожалуй моя вина что я дальше не достаточно ясно раскрыл тему. И так же было видимо зря сразу было предложено конкретное применение подхода, потому что суть статьи была не в нём, а в классе Message. Если я что-то и изобрёл то его. Плюс это рабочий код для отправки писем через Unione, чтобы было легче его применить я привёл его конкретную реализацию NewReserveEmail. И чтобы было ещё легче вникнуть в это я начал с применения.

                    Вы взяли инструмент, но не знаете как ним пользоваться. Но уже считаете, что статьями на хабре можете учить других.

                    Да, я уклонился от изначального замысла представить рабочую реализацию отправки почты через Unione и дал возможность завлечь меня на поле проектирования и архитектуры. Что ж - сам виноват.

                    Вы не понимаете зачем Вам ООП.

                    Так же я не понимаю зачем вам ООП :-)

                    ООП это не только про объекты с методами и классами, хоть и объект является ключевым понятием.

                    Я согласен.


                    1. BoShurik
                      16.09.2022 11:29

                      потому что суть статьи была не в нём, а в классе Message

                      А что в нем? Если убрать отправку письма в отдельный сервис, то что в нем остается?


                      1. mozg3000tm Автор
                        16.09.2022 12:18

                        А что по вашему мнению в нём?

                        Подготовка данных и формирование тела запроса вынесенная в базовый класс. В принципе я это назвал "таким подходом, который не встречался". Я наследуюсь от базового класса и пользуюсь готовой логикой, мне остаётся только определить какие данные откуда будут браться и куда подставляться. Именно такого использования я не встречал и даже в приведённых ссылках.


                      1. BoShurik
                        16.09.2022 12:44

                        Формирование тела запроса мы от туда убрали, т.к. тело запроса зависит от отправителя. Остается подготовка данных. В чем она заключается? Срендерить шаблон и передать туда переменные? Ну я бы это тоже через отдельный сервис сделал, чтобы не зависеть от шаблонизатора.

                        Мне кажется это обычный дата-класс (его даже абстрактным делать не надо, чтобы можно было отправлять простые письма)


                      1. mozg3000tm Автор
                        16.09.2022 14:26

                        Если вы про мою новую реализацию, то я не убирал оттуда формирование тела запроса. Я отнаследовался от класса yii\httpclient\Request представляющего Http запрос.

                        Дальше я переопределил в нём метод setData так чтобы он вызывал метод composeMessage для подготовки тела запроса. Этот класс можно уже использовать для любых http запросов.

                        <?php
                        
                        namespace app\models\Email;
                        
                        use app\interfaces\MessageInterface;
                        use yii\httpclient\Client;
                        use yii\httpclient\Request;
                        
                        class RestMessage extends Request implements MessageInterface
                        {
                            public function __construct(
                                Client $client,
                                $config = []
                            )
                            {
                                parent::__construct($config);
                                $this->client = $client;
                            }
                        
                            public function composeMessage()
                            {
                                return $this->getData();
                            }
                        
                            /**
                             * {@inheritdoc}
                             */
                            public function setData($data)
                            {
                                $this->data = $data;
                                $data = $this->composeMessage();
                        
                                return parent::setData($data);
                            }
                        }

                        Класс UnioneRestMessage это то что раньше у меня называлось Message. Соответственно в этом классе переопределяется метод composeMessage для того, чтобы подготовить тело запроса нужное для Unione.

                        В итоге всё это применяется следующим образом:

                        <?php
                        $client = new RestClient();
                                    $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('секрет')
                                        ->setFormatter(Yii::$app->formatter)
                                        ->setSender('Отсланец')
                                        ->setFrom('info@домен.хз')
                                        ->setSubject($subject)
                                        ->setEmail($user->email)
                                        ->setTemplatePath('/app/mail/unione/user/letter_reserve.php')
                                        ->setData([$user, $investment])
                                    ;
                        $sender = new RestSender($client);
                        $res = $sender->send($newReserveMail);

                        RestSender это не совсем корректное название, потому что он и почту оправляет. Правильнее Sender, не успел ещё переименовать. Тут часть методов наследуется от класса Request, а часть я определил в UnioneRestMessage.


                      1. lair
                        16.09.2022 14:50

                        … а для АПИ-запросов тоже "принято" использовать паттерн, в котором операциям АПИ соответствуют методы, а аргументам (роутингу и пейлоаду) соответствуют параметры методов. А вся конверсия и формирование соответствующего транспортного запроса происходит внутри апи-клиента (и иногда пользователь даже не знает, какая там вариация HTTP). Так что наследование от RestMessage не особо нужно.


                        Типичный, кстати, пример на prefer composition over inheritance.


                      1. lair
                        16.09.2022 14:12

                        Именно такого использования я не встречал и даже в приведённых ссылках.

                        Потому что как-то так сложилось, что для документов (а емейл — это документ) используют генераторы.


                        Вот какая выгода в том, что ваш класс NewReserveMail именно наследуется от Message, а не создает его (предпочтительно иммутабельный) экземпляр, который заполняет данными?


    1. lair
      15.09.2022 14:22

      Я в заголовке написал про отправку писем, и как то дальше это стало пониматься как отправка писем через почту по smpt

      Неа, не стало. Я же не зря вам приводил примеры с SendGrid и SNS — у них у обоих "внизу" HTTP API.