A real-world use case for sending email notifications when a new entry draft is created using a Craft module and an event handler. It requires setting up a module and writing some PHP code, so pull up your sleeves and dust off your IDE.
The challenge is to create a module that listens for the EntryRevisions::EVENT_AFTER_SAVE_DRAFT
event. Whenever the event is fired, provided that the draft is new, the module should send a notification email to the system email address with the subject “First Draft” and the following message:
A new entry draft “{title}” has been created by “{username}”: {cpUrl}
Where {title}
is replaced by the title of the entry draft, {username}
is replaced by the username of the user that created the draft and {cpUrl}
is the URL to edit the entry in the control panel.
For bonus points, if the environment variable FIRST_DRAFT_EMAIL_TEMPLATE
is set then the module should use the rendered template (as defined by the the environment variable) as the email’s HTML body, providing the 3 variables above as template variables.
The module must be a single, self-contained file that sends an email notification as described above whenever a draft is created, if and only if the draft is new (not a revision of an existing draft). It should not rely on any plugins and the code will be evaluated based on the following criteria in order of priority:
Therefore the code should use native Craft components wherever possible. It should be readable and easy to understand, concise and non-repetative.
Every installation of Craft created using the Craft project as documented in the installation instructions comes with a modules/Module.php
file. You can use this file as a starting point or read the guide on how to build a module and create your own.
For an in-depth explanation of modules you can read the article Enhancing a Craft CMS 3 Website with a Custom Module.
Setting up the basic file structure, class autoloading and application config for a module is covered in the docs. We begin with the basic outline of a module class.
<?php
namespace modules;
class Module extends \yii\base\Module
{
public function init()
{
parent::init();
// Custom initialization code goes here...
}
}
The first step is to create an event listener that will be called whenever the EntryRevisions::EVENT_AFTER_SAVE_DRAFT
event is triggered. We do this in the init()
method of the module (so that it will be attached to every request to the CMS) using Event::on
and pass in 3 parameters: the class name which contains the event; the name of the event; and a handler that will handle the logic of what we want the event to do. The handler can be anything that is callable
and we will use an anonymous function (or closure) to handle the logic.
Event::on(EntryRevisions::class, EntryRevisions::EVENT_AFTER_SAVE_DRAFT, function() {});
Looking at the source code of the event, we can see that it passes in a new object of type DraftEvent
with 2 parameters: the entry draft and a boolean representing whether the draft is new.
// Fire an 'afterSaveDraft' event
if ($this->hasEventHandlers(self::EVENT_AFTER_SAVE_DRAFT)) {
$this->trigger(self::EVENT_AFTER_SAVE_DRAFT, new DraftEvent([
'draft' => $draft,
'isNew' => $isNewDraft,
]));
}
So we can update our event handler function to accept a parameter of type DraftEvent
which we will call $event
.
Event::on(EntryRevisions::class, EntryRevisions::EVENT_AFTER_SAVE_DRAFT, function(DraftEvent $event) {});
We can now begin adding the logic. We first ensure that this is a new draft using the event’s isNew
attribute and exit with a return
statement if it is not. Then we fetch the event’s draft
attribute and pass it to a method (that we will create in the same class) that will handle sending the email notification for us.
Event::on(EntryRevisions::class, EntryRevisions::EVENT_AFTER_SAVE_DRAFT, function(DraftEvent $event) {
// Return if this is not a new draft
if ($event->isNew === false) {
return;
}
// Get the draft from the event
$draft = $event->draft;
// Send a notification email
$this->sendNotificationEmail($draft);
});
Next we create the new method to handle the email notification. The method accepts a parameter of type EntryDraft
, as that is what we sent it from our event handler above, and proceeds to do the following:
fromEmail
value from the email system settings.getIdentity()
on the user component. t
method (shortcut for translate) from the provided parameters.public function sendNotificationEmail(EntryDraft $draft)
{
// Create a mailer
$mailer = Craft::$app->getMailer();
// Get system email address
$systemEmailAddress = Craft::$app->getSystemSettings()->getEmailSettings()->fromEmail;
// Get the current user
$user = Craft::$app->getUser()->getIdentity();
// Create the parameters for the notification message
$params = [
'title' => $draft->title,
'username' => $user->username,
'cpUrl' => $draft->getCpEditUrl(),
];
// Create the notification message
$message = Craft::t('app', 'A new entry draft "{title}" has been created by "{username}": {cpUrl}', $params);
// Compose and send a notification email
$mailer->compose()
->setTo($systemEmailAddress)
->setSubject('First Draft')
->setHtmlBody($message)
->send();
}
Similar solutions: Spenser Hannon.
For the bonus points mentioned in the challenge, we first check if the environment variable FIRST_DRAFT_EMAIL_TEMPLATE
exists. If it does then we to render it, given the parameters, to produce and overwrite the email notification message. To render the front-end template, we need to ensure that the view’s template mode is set to site
first. We do this by storing the current template mode as $oldTemplateMode
using the getTemplateMode()
method, then setting the template mode to site
as defined by the View::TEMPLATE_MODE_SITE
constant using the setTemplateMode()
method. After rendering the template, we then restore the template mode to its original value.
// Overwrite the message with the rendered template as defined by the environment variable if it exists
$template = getenv('FIRST_DRAFT_EMAIL_TEMPLATE');
if ($template !== false) {
// Set Craft to the front-end site template mode
$view = Craft::$app->getView();
$oldTemplateMode = $view->getTemplateMode();
$view->setTemplateMode(View::TEMPLATE_MODE_SITE);
$message = $view->renderTemplate($template, $params);
// Restore the original template mode
$view->setTemplateMode($oldTemplateMode);
}
Putting this all together gives us the final solution. Note that the class names have been shortened with the use
operator to make the code more readable.
<?php
namespace modules;
use Craft;
use craft\events\DraftEvent;
use craft\models\EntryDraft;
use craft\services\EntryRevisions;
use craft\web\View;
use yii\base\Event;
class Module extends \yii\base\Module
{
public function init()
{
parent::init();
Event::on(EntryRevisions::class, EntryRevisions::EVENT_AFTER_SAVE_DRAFT, function(DraftEvent $event) {
// Return if this is not a new draft
if ($event->isNew === false) {
return;
}
// Get the draft from the event
$draft = $event->draft;
// Send a notification email
$this->sendNotificationEmail($draft);
});
}
public function sendNotificationEmail(EntryDraft $draft)
{
// Create a mailer
$mailer = Craft::$app->getMailer();
// Get system email address
$systemEmailAddress = Craft::$app->getSystemSettings()->getEmailSettings()->fromEmail;
// Get the current user
$user = Craft::$app->getUser()->getIdentity();
// Creat the parameters for the notification message
$params = [
'title' => $draft->title,
'username' => $user->username,
'cpUrl' => $draft->getCpEditUrl(),
];
// Create the notification message
$message = Craft::t('app', 'A new entry draft "{title}" has been created by "{username}": {cpUrl}', $params);
// Overwrite the message with the rendered template as defined by the the environment variable if it exists
$template = getenv('FIRST_DRAFT_EMAIL_TEMPLATE');
if ($template !== false) {
// Set Craft to the front-end site template mode
$view = Craft::$app->getView();
$oldTemplateMode = $view->getTemplateMode();
$view->setTemplateMode(View::TEMPLATE_MODE_SITE);
$message = $view->renderTemplate($template, $params);
// Restore the original template mode
$view->setTemplateMode($oldTemplateMode);
}
// Compose and send a notification email
$mailer->compose()
->setTo($systemEmailAddress)
->setSubject('First Draft')
->setHtmlBody($message)
->send();
}
}
Solution submitted by Spenser Hannon on 21 November 2018.
<?php
namespace modules;
use Craft;
use craft\events\DraftEvent;
use craft\mail\Message;
use craft\services\EntryRevisions;
use yii\base\Event;
class Module extends \yii\base\Module
{
public function init()
{
parent::init();
Event::on(EntryRevisions::class, EntryRevisions::EVENT_AFTER_SAVE_DRAFT, function (DraftEvent $e) {
if ($e->isNew) {
$replacements = array(
'{title}' => $e->draft->title,
'{username}' => $e->draft->author->username,
'{cpUrl}' => $e->draft->cpEditUrl
);
$subject = strtr('A new entry draft "{title}" has been created by "{username}"', $replacements);
$message = $subject . strtr(': <a href="{cpUrl}">{cpUrl}</a>', $replacements);
$this->sendEmail($message, $subject);
}
});
}
/**
* @param $html
* @param $subject
* @param null $mail
* @param array $attachments
* @return bool
*/
private function sendEmail($html, $subject): bool
{
$emailSettings = Craft::$app->systemSettings->getSettings('email');
$message = new Message();
$message->setFrom([$emailSettings['fromEmail'] => $emailSettings['fromName']]);
$message->setTo(getenv('FIRST_DRAFT_EMAIL_TEMPLATE') ?? $emailSettings['fromEmail']);
$message->setSubject($subject);
$message->setHtmlBody($html);
return Craft::$app->mailer->send($message);
}
}