Как стать автором
Обновить

Комментарии 77

ЗакрепленныеЗакреплённые комментарии

Я в заголовке написал про отправку писем, и как то дальше это стало пониматься как отправка писем через почту по 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 внутри себя. Хотя конечно это было написано давно...

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

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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

/**
* Constructor
*/
public function __construct(bool $useTemplate)
{
  parent::__construct();

  $this->useTemplate = $useTemplate;

  try {
      $this->mailer = Yii::$container->get(EmailServiceInterface::class);

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

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


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

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

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

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

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

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

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

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

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

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

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

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), но то и другое это объекты в которые что-то пробрасывается, и потом вызывается его метод. Если критики это не понимают, то значит эта статья попала в точку и я рад что я описал этот конкретный случай.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

Если я каким-то образом дал понять, что я предлагаю что-то подобное, то извините, не хотел вас ввести в заблуждение. Статья называется - "Использование ООП подхода для рассылки писем через 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е — изменить метод Message::composeMessage
2е — изменить сопоставление EmailServiceInterface => UniOneService на новый сервис

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


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


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

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


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

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


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

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


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

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

Нет, это не инъекция (в терминах паттерна 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 - это когда контейнер зависимостей сам создаёт объекты просто по факту их наличия в определении метода, так, что не нужно сервис локатор использовать.

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

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


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

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

Там дальше по тексту отдельно упоминается 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.

сервис локатор тут не упоминается, на сколько 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);
    [...]
}

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

По поводу

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

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

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

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

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

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

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

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

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

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


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

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


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

Я как раз стараюсь думать, что люди могут хорошо писать на любом языке, и не надо поощрять предрассудки вида "если 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);

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

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


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

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

Я в заголовке написал про отправку писем, и как то дальше это стало пониматься как отправка писем через почту по 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 внутри себя. Хотя конечно это было написано давно...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Я согласен.

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

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

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

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

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

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

Если вы про мою новую реализацию, то я не убирал оттуда формирование тела запроса. Я отнаследовался от класса 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.

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


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

В моей изначальной реализации я не наследовался от Request (RestMessage нужен лишь чтобы прикрутить MessageInterface) а как раз получал его внутрь Message через UniOneService, и класс Меssage даже не знал какая там вариация HTTP.

Я так понял если не вызывать там метод Message::send, а сделать как в моей второй реализации sender->send($message), то это будет то что вы одобрите?

Где "там"?

Получается в классе HttpClient или UniOneService. HttpClient используется и в других классах помимо UniOneService.

Внутри него (HttpClient) ещё один класс клиента yii2\httpclient\Client

На этой диаграмме совершенно не понятно, зачем вам mailer в Message.


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

На этой диаграмме совершенно не понятно, зачем вам mailer в Message

С помощью него отправка запроса происходит в методе Message::sendMessage который вам не понравился с самого начала.

Ну вот как я тогда считал, что это неправильно, так и сейчас так считаю.

Я тут не по этот метод. А по то удовлетворяет ли класс UniOneService вашему утверждению

А вся конверсия и формирование соответствующего транспортного запроса происходит внутри апи-клиента

Не видя кода UniOneService (который должен быть UniOneClient), и не зная, что такое data, ответить на этот вопрос невозможно.

<?php

namespace app\services\backend\email;

use app\services\backend\infrastructure\ClientInterface;
use app\services\backend\infrastructure\HttpClient;
use yii\httpclient\Client;
use yii\httpclient\Response;

class UniOneService implements EmailServiceInterface
{
    /** @var HttpClient */
    private ClientInterface $client;

    /**
     * @param ClientInterface|HttpClient $client
     */
    public function __construct($client)
    {
        $this->client = $client;
    }

    public function initClient(string $apiKey, string $baseUrl)
    {
        $this->client
            ->createRequest()
            ->setBaseUrl($baseUrl)
            ->setPath('email/send.json')
             ->setMethod('POST')
            ->setFormat(Client::FORMAT_JSON)
             ->setHeaders([
                 'Content-Type' => 'application/json',
                 'Accept'       => 'application/json',
                 'X-API-KEY'    => $apiKey
             ]);
    }

    /**
     * @throws \yii\httpclient\Exception
     */
    public function sendEmail($data): Response
    {
        return $this->client->send($data);
    }
}

что такое data

это тело запроса

это тело запроса

Тогда это не то, про что я говорю, потому что вы всего лишь сделали тривиальную обертку вокруг HTTP-клиента, которая ничего не преобразует (т.е. вызывающий все еще должен знать про то, что там внутри JSON, и какого он формата).


А должно быть вот так:


unione.Send(new UniOneMessage {
  From = new Address("Us", "us@we.con"),
  Recipients = new [] {
    new Recipient("Honored", "honor@harring.ton") {
       {"some_substition", "its_value"}
    },
  },
  Subject = "...",
  Body = new PlainTextBody("..."),
...

И так далее. Клиент должен знать про объекты, т.е. про контракт, а не про JSON-представление.

Понятно. Спасибо за наводку. Это конечно много новых классов получается нужно сделать. Прям целая библиотека)

Что будете делать, если письмо, к примеру, не отправится? А бизнес говорит, что надо обязательно отправить. Ретраить внутри Message? Зачем Вам обертка над http клиентом фреймворка, которая просто дублирует (?!) единственный метода родителя? Вы действительно думаете, что реализации http клиента очень часто меняются (PSR-7, PSR-18)? Зачем Вам сервис отправки сообщений, если это просто тот же (завязанный на Message), ничем не отличающийся, вынесенный функционал в отдельный класс?

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

Буду вам благодарен если вы приведёте пример что вы имели ввиду в этом контексте.

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

Вам обертка над http клиентом фреймворка, которая просто дублирует (?!) единственный метода родителя?

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

Вы действительно думаете, что реализации http клиента очень часто меняются (PSR-7, PSR-18)?

Я думаю что я могу задействовать например Guzzle\HttpClient для асинхронной отправки. И поскольку я использовал интерфейс, то я смогу это сделать не меняя определений.

 Зачем Вам сервис отправки сообщений, если это просто тот же (завязанный на Message), ничем не отличающийся, вынесенный функционал в отдельный класс?

Если вы про этот сервис RestSender

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

то он не завязан на Message. с помощью него я могу отправлять smtp письма и любые рест запросы. Вот пример:

<?php
// отправка почты
$emailClient = Yii::$app->mailer;
$emailMessage = new MailMessage();
$emailMessage
    ->setFrom('from@email.ru')
    ->setTo('to@email.ru')
    ->setSubject('Hi there')
    ->setTextBody('Test message');

$sender = new RestSender($emailClient);
$res = $sender->send($emailMessage);

// http запрос
$restClient = new RestClient();
$client->baseUrl = 'https://habr.com/';
$restRequest = new RestMessage($restClient);
$restRequest
    ->setUrl('ru/post/688090/');

$sender = new RestSender($restClient);
$res = $sender->send($restRequest);

приведёте пример что вы имели ввиду в этом контексте.

Ну вот Вы посылаете запрос на Unione. Что-то идёт не так, и Ваш код отработал, а письмо не ушло. Вам приходится более пристально следить, где будет вызываться отправка сообщения самим сообщением.

То, что Вы описали есть к примеру в AWS (App -> SQS -> Lambda -> SES). А Unione придётся ретраить отправку http-запроса. И тогда она у Вас окажется в Message.

Я думаю что я могу задействовать например Guzzle\HttpClient для асинхронной отправки.

Именно. Не нужна своя прослойка. Просто используйте ClientInterface(посмотрите, какой интерфейс у большинства популярных http-клиентов). Если изменится клиент (к примеру Вы попробуете symfony/http-client) - у Вас в любом случае будет ClientInterface. Для этого и существуют PSR/PER (хотя им необязательно следовать).

По поводу сервиса. RestSender, насколько я понимаю, внутри Message. И какой-то немного странный: просто обёртка над ClientInterface. Зачем, почему бы не заинжектить его, вместо своего интерфейса? И почему бы не выкинуть его из Message? Оно слишком много знает.

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

Обертка нужна чтоб интерфейс использовать.

Видимо, это какое-то непонятное мне ограничение вашего фреймворка.

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

… вот это мне и не понятно. У вас что, есть насущная необходимость переходить на другой фреймворк?

Возможно появится желание перейти на другой http клиент. А тот что в yii2 зависит чисто от его классов

Возможно появится желание перейти на другой http клиент.

YAGNI. В том смысле, что сейчас думать про другой клиент — вредно, вы все равно не сможете предусмотреть всех особенностей API, и построить хорошую абстракцию.

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

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


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

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

Тут вы видимо опять про проектирование. Если я вас правильно понял и вы об этом. Мне знакома идея замены наследования композицией и должен признаться - я фанат наследования, и для меня сейчас этот как грааль, я имею тягу разработать супер пупер систему наследования. Само это наследование очень похоже на систему понятий и соответствует моему способу мышления в понятиях. Тем не менее я вижу преимущество композиции в следующем. В моей новой реализации класс RestMessage наследуется от класса Request, который разработан не мной, и наследуясь от него я рискую что он в какой-то момент так измениться, что мой код совсем сломается. И если сделать композицию, то можно получить больше возможности разрулить эту ситуацию.

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

Можете пояснить эту мысль, чтобы я её лучше понял?

Можете пояснить эту мысль, чтобы я её лучше понял?

Вместо того, чтобы делать класс Email, в котором наследники (ну скажем, NewUserNotification) будут оверрайдить Subject, To и Body, намного полезнее в долгосрочной перспективе сделать класс Notifier с методом CreateUserNotification (а иногда достаточно и метода в одном месте), который бы просто создавал объект типа Email, которому бы назначал правильные Subject, To и Body.

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории