В статье представлен код позволяющий отправлять транзакционные письма через сервис unione, делать HTTP запросы к REST апи, а так же отправлять обычные email по smtp используя общий класс отправитель различных сообщений.

В нашей предыдущей статье (Использование ООП подхода для рассылки писем через Unione (php, Yii2)) мы представили базовый класс Message наследуясь от которого можно создать письмо для рассылки через сервис unione просто определив в наследниках методы для получения подстановок в шаблоны писем. Для отправки писем использовался метод Message::send вызываемый на объекте класса наследника. Такой подход с отправкой сообщения через сам класс письма вызвал определённую критику.

В данной статье класс Message переработан так, чтобы письмо можно было отправлять через общий класс отправителя. Кроме того приведён код как можно с помощью этого класса отправить email по smtp, а так же HTTP запрос к REST апи.

Поскольку отправка письма через сервис unione фактически является просто отправкой HTTP запроса к REST апи сервиса, то в начале имеет смысл рассмотреть просто отправку любых HTTP запросов. Для их отправки создадим класс, который расширит класс yii\httpclient\Request и имплементирует созданный нами интерфейс app\interfaces\MessageInterface.

<?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);
    }
}

В данном классе переопределяется базовый метод Request::setData так чтобы данные получались с помощью метода RestMessage::composeMessage. Тогда отправка HTTP GET запроса с помощью общего класса отправителя будет выглядеть следующем образом:

<?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);

Теперь реализуем на базовый класс UnioneRestMessage для отправки писем через сервис unione. Для этого унаследуем его от RestMessage. Вот его реализация:

<?php

namespace app\models\Email;

use yii\helpers\ArrayHelper;
use yii\httpclient\Client;
use yii\web\View;

abstract class UnioneRestMessage extends RestMessage
{
    protected array  $data;
    protected $template;
    protected $templatePath;
    protected $email;
    protected $subject;
    protected $from;
    protected $sender;
    protected $formatter;
    protected $useTemplate;
    protected $baseUrl;

    /**
     * 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(): array;
    abstract public function getEmailOptions(): array;

    /**
     * Constructor
     */
    public function __construct(
        Client $client,
        bool $useTemplate = false
    )
    {
        parent::__construct($client);

        $this->useTemplate  = $useTemplate;
    }

    public function composeMessage()
    {
        $this->prepareData($this->data);
        $message = [
            "message" => [
                "recipients"            => $this->recipients,
                "subject"               => $this->subject,
                "from_email"            => $this->from,
                "from_name"             => $this->sender,
                'global_substitutions'  => $this->getGlobalSubstitutions(),
                'options'               => $this->getOptions(),
            ]
        ];
        if ($this->useTemplate) {
            $message['message']['template_id'] = $this->template;

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

    /**
     * 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;
            }
        }
    }

    /**
     * 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 getRecipients(): array
    {
        $recipients = [
            [
                "email"          => $this->email,
                "substitutions"  => $this->substitutions
            ]
        ];
        return $recipients;
    }

    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();
    }

    public function getBaseUrl()
    {
        return $this->baseUrl;
    }

    /**
     * @param mixed|string $templatePath
     */
    public function setTemplatePath($templatePath)
    {
        $this->templatePath = $templatePath;
        return $this;
    }

    /**
     * @param mixed $email
     */
    public function setEmail($email)
    {
        $this->email = $email;
        return $this;
    }

    /**
     * @param mixed $subject
     */
    public function setSubject($subject)
    {
        $this->subject = $subject;
        return $this;
    }

    /**
     * @param mixed|string $from
     */
    public function setFrom($from)
    {
        $this->from = $from;
        return $this;
    }

    /**
     * @param mixed|string $sender
     */
    public function setSender($sender)
    {
        $this->sender = $sender;
        return $this;
    }

    /**
     * @param mixed|string $formatter
     */
    public function setFormatter($formatter)
    {
        $this->formatter = $formatter;
        return $this;
    }

    /**
     * @param mixed|string $apiKey
     */
    public function setApiKey($apiKey)
    {
        $this->getHeaders()->set('X-API-KEY', "{$apiKey}");
        return $this;
    }
}

В этом классе как мы видим переопределяется метод RestMessage::composeMessage так чтобы в общем определить структуру тела запроса к апи сервиса, а получение конкретных данных вынесено в классы наследники представляющие собой конкретные типы писем, через объявленные абстрактными методы UnioneRestMessage::getProperties и UnioneRestMessage::getSubstitutions. Кроме того, по сравнению с классом Message тут также добавлены методы, которые раньше были в UniOneService такие как setSubject, setFrom, setSender и т.д.

Для того чтобы было понятно как пользоваться данным базовым классом хотим привести реализацию письма об успешной инвестиции пользователя в проект.

<?php

namespace app\models\Email;

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

class NewReserveMailRestMessage extends UnioneRestMessage
{

    /**
     * @inheritDoc
     */
    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);
                },
                'id_label' => function (InvestmentReserve $reserve) {
                    return $reserve->project->id_label;
                },
                'country' => function (InvestmentReserve $reserve) {
                    return $reserve->project->country->country;
                },
                'target' => function (InvestmentReserve $reserve) {
                    return $this->formatter->asDecimal($reserve->project->amount, 2);
                },
                'loan' => function (InvestmentReserve $reserve) {
                    return $reserve->project->loan_period;
                },
                'rate' => function (InvestmentReserve $reserve) {
                    return $reserve->project->profitability;
                },
                'image' => function (InvestmentReserve $reserve) {
                    $base = Yii::$app->params['api_url'];
                    return $base . $reserve->project->image;
                },
                'left' => function (InvestmentReserve $reserve) {
                    return $this->formatter->asDecimal($reserve->project->availableAmount->available_amount, 2);
                },
                'name' => function (InvestmentReserve $reserve) {
                    return $reserve->project->name;
                },
                'label' => function (InvestmentReserve $reserve) {
                    return $reserve->project->id_label;
                }
            ]
        ];
    }

    /**
     * @inheritDoc
     */
    public function getSubstitutions(): array
    {
        $baseUrl = Yii::$app->params['front_url'];
        $apiBaseUrl = Yii::$app->params['api_url'];
        return [
            "Name"               => $this->getSubstitution('fullname'),
            "Invested_amount"    => $this->getSubstitution('amount'),
            "P1_label"           => $this->getSubstitution('id_label'),
            "P1_name"            => $this->getSubstitution('name'),
            "P1_where"           => $this->getSubstitution('country'),
            "P1_left"            => $this->getSubstitution('left'),
            "P1_target"          => $this->getSubstitution('target'),
            "P1_loan_period"     => $this->getSubstitution('loan'),
            "P1_interest_rate"   => $this->getSubstitution('rate'),
            "P1_link_img"        => $this->getSubstitution('image'),
            "Account_balance"    => $this->getSubstitution('user_funds'),
            'api_url'            => $apiBaseUrl,
            'front_url'          => $baseUrl,
            'P1_link'            => $baseUrl . '/projects/project/' . $this->getSubstitution('id_label'),
            'logo'               => $baseUrl . '/images/logo.svg'
        ];
    }

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

Метод $this->getSubstitution возвращает значение поля объявленного в методе getProperties.

Теперь мы можем отправить это письмо через сервис следующим образом:

<?php

$investment = InvestmentReserve::findOne(102);
$project    = $investment->project;
$user       = $investment->user;

$subject = "You invested in the Project \"$project->name\" $project->id_label";

$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('secret')
    ->setFormatter(Yii::$app->formatter)
    ->setSender('Company name')
    ->setFrom('email@address.com')
    ->setSubject($subject)
    ->setEmail($user->email)
    ->setTemplatePath('/app/mail/unione/user/letter_reserve.php')
    ->setData([$user, $investment])

  $sender = new Sender($client);
  $res = $sender->send($newReserveMail);

Здесь методы setMethod, setUrl и setFormat наследуются от yii\httpclient\Request, setData определён в RestMessage, остальные в UnioneRestMessage.

С помощью класса UnioneRestMessage можно создавать письма для рассылки через сервис рассылок unione просто наследуясь от него и определяя методы getProperties и getSubstitutions и отправлять их с помощью общего класса отправителя Sender в котором может быть реализована логика отправки HTTP сообщений (запросов). Кроме того с помощью этого класса Sender можно также отправлять и email по smtp. Вот пример как это можно реализовать:

<?php

$client = Yii::$app->mailer;

$emailMessage = new MailMessage();
$emailMessage
    ->setFrom('from@email.ru')
    ->setTo('to@email.ru')
    ->setSubject('Hi there')
    ->setTextBody('Test message');

$sender = new Sender($client);
$res = $sender->send($emailMessage);

Здесь MailMessage просто обёртка над yii\swiftmailer\Message реализующая наш app\interfaces\MessageInterface

<?php
<?php

namespace app\models\Email;

class MailMessage extends \yii\swiftmailer\Message implements \app\interfaces\MessageInterface
{

}

Таким образом, с помощью класса Sender мы смогли отправить и простой запрос к http ресурсу, и подготовленный запрос к сервису unione, и email по smtp. Давайте в заключение рассмотрим его минимальную реализацию для того чтобы пример с отправкой писем через unione был полностью рабочим.

<?php

namespace app\services\backend\email;

use app\interfaces\MessageInterface;
use app\interfaces\RestSenderInterface;
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 RestSenderInterface
{
    /** @var Client $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);
    }
}

Здесь в конструктор передаётся объект реализующий наш интерфейс \app\services\backend\infrastructure\ClientInterface, поэтому для того чтобы предыдущие примеры работали для фреймворка Yii2 пришлось переопределить их yii\httpclient\Client и компонент мейлера так чтобы они реализовывали этот ClientInterface интерфейс следующим образом.

// http client
<?php

namespace app\services\backend\infrastructure;

use yii\httpclient\Client;

class RestClient extends Client implements ClientInterface
{

}
// email client
<?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 
use app\services\backend\email\MailSender;
// Конфигурация ...
'mailer' => [
        'class'            => MailSender::class,
//        'class'            => 'yii\swiftmailer\Mailer',
        'useFileTransport' => false,
        'htmlLayout'       => 'layouts/html',
        'transport'        => [...],
    ],

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


  1. mozg3000tm Автор
    26.09.2022 10:37
    -1

    Целью статьи является представление практической реализации отправки письма через web api сервиса unione и в ней не рассматриваются проблемы использования наследования для создания новых классов. Общий класс отправителя Sender использован для примера отправки различных сообщений и не обязателен для использования.

    Для отправки писем через апи сервиса можно использовать yii\httpclient\Client


  1. lair
    25.09.2022 15:51

    Зачем класс RestMessage знает про Client? Зачем UnioneRestMessage наследуется от RestMessage (и, как следствие, должен знать про Client)? Какую полезную абстрацию предоставляет класс RestMessage по сравнению с классом Request? Где посмотреть на определения интерфейсов?


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

      Зачем класс RestMessage знает про Client?

      Это артефакт базового класса Request https://www.yiiframework.com/extension/yiisoft/yii2-httpclient/doc/api/2.0/yii-httpclient-request

      Разработчики yii2 так сделали, повторив мою реализацию (или я их). Там даже метод send есть.


      1. lair
        25.09.2022 17:28

        Это артефакт базового класса Request

        А зачем вам наследоваться от Request? Зачем вам эта зависимость?


        1. mozg3000tm Автор
          25.09.2022 17:56

          yii\swiftmailer\Message и yii\httpclient\Request стандартные юишные компоненты. В данном случае моя реализация на yii2, поэтому я сделал с использованием стандарных юишных компонентов.


          1. lair
            25.09.2022 17:56

            Для того, чтобы использовать компоненты, не обязательно от них наследоваться.


    1. mozg3000tm Автор
      25.09.2022 17:23

      Зачем UnioneRestMessage наследуется от RestMessage [...]?

      Ответ есть тут:

      Здесь (в объекте $newReserveMail - прим. автора) методы setMethod, setUrl и setFormat наследуются от yii\httpclient\Request, setData определён в RestMessage, остальные в UnUnioneRestMessage.

      Т.е. я использую уже готовые методы родителя. Их гораздо больше чем у меня в примере.


      1. lair
        25.09.2022 17:31
        +1

        А зачем пользователю вашего класса вызывать эти методы? Разве метод, адрес и формат сообщение не заданы целиком и полностью АПИ UniOne?


        Весь смысл создания программного интерфейса в вашем языке вокруг HTTP API в том, чтобы пользователь класса не знал про это. Пользователь должен вызывать uniOne.sendMail, а все потроха типа POST <url> и форматирования в JSON — обязанность вашей обертки.


        1. mozg3000tm Автор
          25.09.2022 17:46

          А зачем пользователю вашего класса вызывать эти методы?

          Принял.

          Тем не менее ответ на вопрос тот же

          Зачем UnioneRestMessage наследуется от RestMessage [...]?

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


          1. lair
            25.09.2022 17:50
            +1

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

            Чтобы использовать методы, не нужно наследование.


            Это вообще распространенная такая ошибка: наследование (в ООП) нужно не для того, чтобы был легкий доступ к методам или для переиспользования кода, наследование нужно в первую очередь для того, чтобы можно было использовать наследник там, где используется базовый класс. Какие места в вашем коде принимают RestMessage?


            1. mozg3000tm Автор
              25.09.2022 17:58

              <?php
              public function send(MessageInterface $message){
                    /** @var RestMessage|UnioneRestMessage|MailMessage $message */
                    return $this->client->send($message);
              }


              1. lair
                25.09.2022 18:00

                Это место принимает не RestMessage, а MessageInterface. Почему не реализовать MessageInterface сразу на UnioneRestMessage?


                1. mozg3000tm Автор
                  25.09.2022 18:07

                  Для того чтобы с помощью 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 запросов к любому апи.


                  1. lair
                    25.09.2022 18:09

                    А зачем это нужно? Вы решаете задачу рассылки писем через Unione, а не написания абстрактного REST-клиента (я не верю, что в вашем фреймворке — или для него — уже нет готового клиента, зачем новый изобретать?).


                    Более того, даже для задачи отправки любых запросов не нужно наследовать UnioneRestMessage от RestMessage, достаточно в них обоих реализовать MessageInterface.


                    Но это опять упирается в то, что ваш MessageInteface сам по себе бесполезен.


                    1. mozg3000tm Автор
                      25.09.2022 19:12

                      А зачем это нужно?

                      Если я наследуюсь от Request, то логично использовать возможность отправки любых http запросов, потому что этот класс и предназначен для этого. Если не наследоваться то не логично. Но я наследуюсь. Наследование не только для полиморфизма, но и для переиспользования кода.

                      для задачи отправки любых запросов не нужно наследовать UnioneRestMessage от RestMessage, достаточно в них обоих реализовать MessageInterface.

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

                      Вы решаете задачу рассылки писем через Unione, а не написания абстрактного REST-клиента

                      Пожалуй да. но интересно попробовать что-то большее. Хотя видимо тут есть момент что я не правильно видимо понял ваше предложение вынести метод send из класса Message.


                      1. lair
                        25.09.2022 19:17
                        +1

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

                        Нет, не логично. Это не входит в вашу задачу.


                        Наследование не только для полиморфизма, но и для переиспользования кода.

                        Вот это как раз фундаментальная ошибка. Наследование в ООП — для полиморфизма. Для переиспользования кода есть другие механизмы.


                        Методы по формированию запроса находятся в Request классе

                        А зачем вам в UnioneRestMessage использовать именно эти методы?


                        Пожалуй да. но интересно попробовать что-то большее.

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


                      1. mozg3000tm Автор
                        25.09.2022 19:30

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

                        Да, но допустим вы же не предлагаете такое для контролеров, ORM моделей и вьюшек. Когда вы делаете MVC (MVVM) с сразу наследуетесь от них. А можно было бы делать из них композицию и в своих классах без наследования уже вызывать нужные методы. Но вы хотите использовать подготовленную логику без вникания в неё.


                      1. lair
                        25.09.2022 19:43

                        Да, но допустим вы же не предлагаете такое для контролеров

                        Во-первых, это зависит от контроллера (иногда достаточно реализовать интерфейс). Во-вторых, в тех случаях, когда наследование полезно, оно полезно именно потому, что мы переиспользуем не код, а поведение: т.е. каждый контроллер должен обладать вот таким поведением, а мы его расширяем. У вас не так.


                        Иными словами, я наследуюсь от контроллера не для того, чтобы "вызывать нужные методы", а для того, чтобы фреймворк вызывал нужные методы у базового контроллера, а тот уже роутил их на мои методы. Inversion of Control в чистом виде.


                        ORM моделей и вьюшек

                        А вот для моделей наследование от общего предка уже давно антипаттерн. RTFM persistence ignorance.


    1. mozg3000tm Автор
      25.09.2022 17:30

      Какую полезную абстрацию предоставляет класс RestMessage по сравнению с классом Request?

      RestMessage реализует интерфейс MessageInterface чтобы это определение работало для любых $message.

      <?php
      public function send(MessageInterface $message){
            /** @var RestMessage|UnioneRestMessage|MailMessage $message */
            return $this->client->send($message);
      }

      И с помощью него можно отправлять http запросы через общий клиент.


      1. lair
        25.09.2022 17:31

        Какую конкретно абстрацию предлагает интерфейс MessageInterface? Где его определение?


    1. mozg3000tm Автор
      25.09.2022 17:35

      Где посмотреть на определения интерфейсов?

      Они пустые. В основном.


      1. lair
        25.09.2022 17:41

        Пустой интерфейс не дает полезной абстракции. В чем его смысл?


        1. mozg3000tm Автор
          25.09.2022 17:54

          Смысл в том, что между конкретными юишными реализациями yii\swiftmailer\Message и yii\httpclient\Request нет связи, и чтобы их принимать на вход метода send(MessageInterface $message) нужно их связать общим предком.


          П.с. yii\swiftmailer\Message и yii\httpclient\Request стандартные юишные компоненты. В данном случае моя реализация на yii2, поэтому я сделал с использованием стандарных юишных компонентов.


          1. lair
            25.09.2022 17:56

            нужно их связать общим предком.

            Как вы можете взаимодействовать с этим "общим предком", если у него нет никаких операций и свойств? Как выглядит метод send?


            1. mozg3000tm Автор
              25.09.2022 17:59

              <?php
              public function send(MessageInterface $message){
                    /** @var RestMessage|UnioneRestMessage|MailMessage $message */
                    return $this->client->send($message);
              }


              1. lair
                25.09.2022 18:01

                Что такое client? Какого оно типа? Какого типа параметр в его методе send?


                1. mozg3000tm Автор
                  25.09.2022 18:13

                  Что такое 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);
                  }


                  1. lair
                    25.09.2022 18:16

                    interface ClientInterface
                    {
                    public function send($message);
                    }

                    Какого типа параметр message?


                    class MailSender extends Mailer implements ClientInterface
                    {
                    public function send($message)
                    {
                    $this->sendMessage($message);
                    }
                    }

                    Какого типа параметр message в операции sendMessage?


                    1. mozg3000tm Автор
                      25.09.2022 18:29

                      Пожалуй я тут воспользовался возможностью php не указывать тип.

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

                      Какого типа параметр message в операции sendMessage?

                      \yii\mail\MessageInterface

                      а вот у yii\http\client -> \yii\httpclient\Request


                      1. lair
                        25.09.2022 18:31

                        \yii\mail\MessageInterface
                        а вот у yii\http\client -> \yii\httpclient\Request

                        … и что будет, если я передам \yii\httpclient\Request в MailSender?


                      1. mozg3000tm Автор
                        25.09.2022 18:39

                        Думаю что тоже самое как если его передали в класс Mailer.


                      1. lair
                        25.09.2022 18:40
                        +2

                        И это значит, что ваша абстрация протекла, а пользователь получил неожиданное поведение. Что намекает нам, что в дизайне ошибка.


                      1. mozg3000tm Автор
                        25.09.2022 19:00

                        ваша абстрация протекла

                        Абстракция чего?


                      1. lair
                        25.09.2022 19:01

                        ClientInterface. Абстрация чего конкретно это я из вашего кода не очень понимаю.


                      1. mozg3000tm Автор
                        25.09.2022 19:19

                        Есть класс Sender в его конструктор я передаю клиента (почтового или http) и с помощью этого клиента отправляю сообщение (письмо или запрос) в методе send.
                        Тут видимо абстрагируются вещи которые и в том и в том случае могут быть сделаны в методе send перед отправкой сообщения и после.


                      1. lair
                        25.09.2022 19:21
                        +1

                        Есть класс Sender в его конструктор я передаю клиента (почтового или http) и с помощью этого клиента отправляю сообщение (письмо или запрос) в методе send.

                        Вот и какой в этом всём смысл? Вашему пользователю все равно надо знать, был там почтовый клиент или http-клиент, чтобы передать соответствующий message.


                        Тут видимо абстрагируются вещи которые и в том и в том случае могут быть сделаны в методе send перед отправкой сообщения и после.

                        … которых у вас нет.


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


                      1. bombe
                        27.09.2022 09:30

                        Пожалуй я тут воспользовался возможностью php не указывать тип.

                        Очень зря. Попробуйте как-то в каждом Вашем PHP файле указать declare(strict_types=1); и типизировать каждый аргумент, каждый return type, каждое свойство класса, заиспользуйте phpstan с уровнем 9, попробуйте решить все ошибки анализатора (он подсказывает, как) и Вы внезапно обнаружите, что язык Вам сам подсказывает где и как лучше сделать.


                      1. mozg3000tm Автор
                        27.09.2022 10:15

                        Спасибо за declare(strict_types=1).

                        Оно как то поможет использовать чужой готовый Легаси код?


                      1. bombe
                        27.09.2022 11:16

                        Оно как то поможет использовать чужой готовый Легаси код?

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

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

                        Т.е. если Вы не укажете declare(strict_types=1) и будете вызывать функцию из файла с указанной строгой типизацией, то будет применяться приведение типов и Вы сможете передать int вместо string.

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


                1. mozg3000tm Автор
                  25.09.2022 18:16

                  Какого типа параметр в его методе send?

                  У меня в интерфейсе тип опущен.


                  1. lair
                    25.09.2022 18:17

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


            1. mozg3000tm Автор
              25.09.2022 18:02

              Как вы можете взаимодействовать с этим "общим предком", если у него нет никаких операций и свойств?

              Хоршогий вопрос. Тут получается что под нужный message нужен правильный клиент, что как странно...


              1. lair
                25.09.2022 18:04
                +2

                Тут получается что под нужный message нужен правильный клиент

                Что означает, что ваша абстрация протекла насквозь (или, иными словами, вы нарушили LSP). Пустые интерфейсы — вполне узнаваемый code smell.


                1. mozg3000tm Автор
                  25.09.2022 18:20

                  Фабричный метод мне в помощь? т.е. в потомках MessageInterface сделать createClient?


                  1. lair
                    25.09.2022 18:22
                    +2

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


                    Возможно, в процессе разбирательства вы все-таки поймете, что MessageInterface вам тоже низачем не нужен.


  1. lair
    25.09.2022 20:04

    У меня к вам есть внезапный вопрос: а вы как-то учитываете в дизайне вашего UnioneRestMessage и его наследников, что Unione, вообще-то, заточен под отправку множества писем одним вызовом?


    1. mozg3000tm Автор
      25.09.2022 20:39

      Не то чтобы он заточен. 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];
          }
      }
      

      Не было возможности его представить, хорошо что спросили :)

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


      1. lair
        25.09.2022 20:46

        Unione как раз заточен под транзакционные письма. Т.е. юзер сделал инвестицию, и ему шлётся письмо.

        … а зачем там тогда массив получателей? Нет, согласно API он заточен под массовую рассылку.


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


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

        … вот как раз: если это базовая функциональность сервиса, почему у вас это отдельный класс?


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


        1. mozg3000tm Автор
          25.09.2022 21:38

          а зачем там тогда массив получателей? Нет, согласно API он заточен под массовую рассылку.

          Мы использовали использовали Unisender для рассылок, в т.ч. единичных писем, и это было не очень юзерфрендли удобно в результате сам сервис рекомендовал свой другой сервис Unione который он и презентовал как сервис для отправки единичных писем назвав их транзакционными. Если вы на основе анализа его апи определили что он заточен пот массовую рассылку, хорошо. перед нами стояла задачи отправлять именно единичные письма одному получателю.

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

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

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

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


          1. lair
            25.09.2022 21:47

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

            … а в чем конкретно состоит этот "функционал", который не требуется?


            И зачем у вас тогда в UnioneRestMessage есть метод getRecipients(): array?


            1. mozg3000tm Автор
              25.09.2022 22:11

              в чем конкретно состоит этот "функционал", который не требуется?

              Конкретно в методах 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()

              Вы знаете зачем :) массив получателей там определяется.


              1. lair
                25.09.2022 22:20

                Конкретно в методах getGlobalSubstitutions() и getGlobalProperties() которые имеют смысл если получателей больше одного и задают подстановки сразу для всех получателей.

                Неа, они имеют смысл всегда. Например, вы можете там задавать те вещи, которые не зависят от конкретного письма (название компании, контактную информацию).


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


                Вы знаете зачем :) массив получателей там определяется.

                А зачем определять массив получаетелей, если получатель всегда один?


                1. mozg3000tm Автор
                  26.09.2022 10:06

                  имеют смысл всегда

                  в случае одного получателя, нет смыла размазывать информацию по нескольким методам. Если у вас другое мнение то попробуйте использовать апи, чтобы примерить ситуацию.

                  пренебрежимо малая функциональность

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

                  получатель всегда один

                  "если получатель один"


                  1. lair
                    26.09.2022 14:10

                    Если у вас другое мнение то попробуйте использовать апи, чтобы примерить ситуацию.

                    Да, у меня другое мнение, и я его даже описал уже.


                    Это выглядит так, что когда вам удобно вы отходите от принципов.

                    И от какого же принципа я здесь отхожу?..


                    Но вообще, да, я стараюсь следовать принципам, когда это оправданно — потому что если бездумно следовать им всегда, получается ничуть не лучше, чем если не следовать им вовсе.


                    "если получатель один"

                    Вы меня запутали. Если получателей много, то должны быть глобальные подстановки и метаданные. Иными словами, эти свойства должны идти вместе.


  1. mozg3000tm Автор
    26.09.2022 10:28

    Хочу отдельно пояснить как можно реализовать отправку сервисных (транзакционных) писем на 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);
    • * можно даже напрямую унаследовать класс UnioneRestMessage от Request, если не планируете использовать отправку других запросов не связанных с апи сервиса. Соответственно код RestMessage нужно будет перенести в UnioneRestMessage.


    1. lair
      26.09.2022 14:11

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


  1. mozg3000tm Автор
    26.09.2022 10:32

    В данной реализации для создания классов писем используется наследование от компонента фрейворка yii\httpclient\Request, поскольку отправка письма через апи сервиса представляет собой частный случай http запроса к апи сервиса.


  1. mozg3000tm Автор
    26.09.2022 10:37
    -1

    Целью статьи является представление практической реализации отправки письма через web api сервиса unione и в ней не рассматриваются проблемы использования наследования для создания новых классов. Общий класс отправителя Sender использован для примера отправки различных сообщений и не обязателен для использования.

    Для отправки писем через апи сервиса можно использовать yii\httpclient\Client