Sending emails via Unione (php, Yii2)
The article presents a code that allows you to send transactional emails through the unione service, make HTTP requests to the REST api, and also send regular emails via smtp using a common sender class for various messages.
In our previous article (Using an OOP approach to send emails via Unione (php, Yii2)) we introduced the base Message class, from which you can create a message to send via the unione service, simply by defining methods in the heirs to get substitutions in email templates. To send letters, the Message::send method called on the object of the inheritor class was used. This approach of sending a message through the mail class itself has caused some criticism.
In this article, the Message class has been redesigned so that an email can be sent through a generic sender class. In addition, the code is given on how to send email via smtp using this class, as well as an HTTP request to the REST api.
Since sending an email through the unione service is actually just sending an HTTP request to the service’s REST api, it makes sense to start with just sending any HTTP requests. To send them, let’s create a class that extends the yii\httpclient\Request class and implements the app\interfaces\MessageInterface interface we created.
<?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);
}
}
This class overrides the base Request::setData method so that the data is obtained using the RestMessage::composeMessage method. Then sending an HTTP GET request using a common sender class will look like this:
<?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);
Now we implement on the base class UnioneRestMessage to send letters through the unione service. To do this, we will inherit it from RestMessage. Here is its implementation:
<?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;
}
}
In this class, as we can see, the RestMessage::composeMessage method is overridden in such a way as to generally determine the structure of the request body to the service api, and the receipt of specific data is moved to the successor classes, which are specific types of messages, through the declared abstract methods UnioneRestMessage::getProperties and UnioneRestMessage:: getSubstitutions. In addition, compared to the Message class, there are also added methods that used to be in UniOneService such as setSubject, setFrom, setSender, etc.
In order to make it clear how to use this base class, we want to provide an implementation of a letter about a user’s successful investment in the project.
<?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/Maclear.svg'
];
}
public function getEmailOptions(): array
{
return [];
}
}
The $this->getSubstitution method returns the value of the field declared in the getProperties method.
Now we can send this email through the service like this:
<?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);
Here the setMethod, setUrl and setFormat methods are inherited from yii\httpclient\Request, setData is defined in RestMessage, the rest in UnioneRestMessage.
Using the UnioneRestMessage class, you can create letters for distribution through the unione mailing service simply by inheriting from it and defining the getProperties and getSubstitutions methods and sending them using the general sender class Sender, in which the logic for sending HTTP messages (requests) can be implemented. In addition, using this Sender class, you can also send email via smtp. Here is an example of how this can be implemented:
<?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);
Here MailMessage is just a wrapper over yii\swiftmailer\Message that implements our app\interfaces\MessageInterface
<?php
<?php
namespace app\models\Email;
class MailMessage extends \yii\swiftmailer\Message implements \app\interfaces\MessageInterface
{
}
Thus, using the Sender class, we were able to send a simple request to an http resource, a prepared request to the unione service, and email via smtp. Let’s finally consider its minimal implementation so that the example of sending emails via unione is completely working.
<?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);
}
}
Here, an object that implements our \app\services\backend\infrastructure\ClientInterface interface is passed to the constructor, so in order for the previous examples to work for the Yii2 framework, we had to redefine their yii\httpclient\Client and the mailer component so that they implement this ClientInterface interface as follows.
// 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);
}
}
And for the mailer component, also correct the definition of the mailer component in the config.
<?php
use app\services\backend\email\MailSender;
// Конфигурация ...
'mailer' => [
'class' => MailSender::class,
// 'class' => 'yii\swiftmailer\Mailer',
'useFileTransport' => false,
'htmlLayout' => 'layouts/html',
'transport' => [...],
],