Sending emails

We wanted to use our MailLogger to send out an email whenever we are logging an error. So let's go back to our class and add this logic.

This is what our log() method can look like now:

/**
* {@inheritdoc}
*/
public function log($level, $message, array $context = array()) {
if ($level !== RfcLogLevel::ERROR) {
return;
}

$to = $this->configFactory->get('system.site')->get('mail');
$langcode = $this->configFactory->get('system.site')->get('langcode');
$variables = $this->parser->parseMessagePlaceholders($message, $context);
$markup = new FormattableMarkup($message, $variables);
\Drupal::service('plugin.manager.mail')->mail('hello_world', 'hello_world_log', $to, $langcode, ['message' => $markup]);
}

First of all, we said that we only want to send mails for errors, so in the first lines, we check whether the attempted log is of that level and return early otherwise. In other words, we don't do anything if we're not dealing with an error and rely on other registered loggers for those.

Next, we determine who we want the email to be sent to and the langcode to send it in (both are mandatory arguments to the mail manager's mail() method). We opt to use the site-wide email address (just as we did for the From value). We also use the same configuration object as we used earlier in the hook_mail() implementation. Don't worry we will shortly take care of injecting the config factory into the class.

When we talk about langcode, we refer to the machine name of a language object. In this case, that is what is being stored for the site-wide default language. Also, we'll default to that for our emails. In a later chapter, we will cover more aspects regarding internationalization in Drupal 8.

Then, we prepare the message that is being sent out. For this, we use the FormattableMarkup helper class to which we pass the message string and an array of variable values that can be used to replace the placeholders in our message. We can retrieve these values using the LogMessageParser service the same way as the DbLog logger does. So with this, we are basically extracting the placeholder variables from the entire context array of the logged message.

Lastly, we use the mail manager plugin to send the email. The first parameter to its mail() method is the module we want to use for the mailing. The second is the key (or template) we want to use for it (which we defined in hook_mail()). The third and fourth are self-explanatory, while the fifth is the $params array we encountered in hook_mail(). If you look back at that, you'll note that we used the message key as the body. Here, we populate that key with our markup object, which has a _toString() method that renders it with all the placeholders replaced.

You may wonder why I did not inject the Drupal mail manager as I did the rest of the dependencies. Unfortunately, the core mail manager uses the logger channel factory itself, which in turn depends on our MailLogger service. So if we make the mail manager a dependency of the latter, we find ourselves in a circular loop. So when the container gets rebuilt, a big fat error is thrown. It might still work, but it's not alright. So, I opted to use it statically, because, in any case, this method is very small and would be difficult to test due to its expected result being difficult to assert (it sends an email). Sometimes, you have to make these choices, as the alternative would have been to inject the entire service container just to trick it. However, that is a code smell and would not have helped anyway had I wanted to write a test for this class.

Even if I did not inject the mail manager, I did inject the rest. So, let's take a look at what we need now at the top of the class:

/**
* @var \Drupal\Core\Logger\LogMessageParserInterface
*/
protected $parser;

/**
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;

/**
* MailLogger constructor.
*
* @param \Drupal\Core\Logger\LogMessageParserInterface $parser
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
*/
public function __construct(LogMessageParserInterface $parser, ConfigFactoryInterface $config_factory) {
$this->parser = $parser;
$this->configFactory = $config_factory;
}

And finally, all the relevant use statements that we were missing:

use Drupal\Core\Logger\LogMessageParserInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Logger\RfcLogLevel;

Finally, let's quickly also adjust the service definition of our mail logger:

hello_world.logger.hello_world:
class: Drupal\hello_world\Logger\MailLogger
arguments: ['@logger.log_message_parser', '@config.factory']
tags:
- { name: logger }

We simply have two new arguments—nothing new to you by now.

Clearing the caches and logging an error should send the logged message (with the placeholders replaced) to the site email address (and from the same address) using the PHP native mail() function. Congratulations! You just sent out your first email programmatically in Drupal 8.