Adding a New Email Template
In this cookbook, we will add a new email template that allows us to alert the customer when his password change.
We want to send an email to the user when his password is reset. This email should be configurable in administration and we should be able to personalize email – we want to be able to include customer email and full name into the email.
New Mail class¶
The first step to create a new email template is creating a class able to create a MessageData
object that can be then sent with Mailer
.
declare(strict_types=1);
namespace App\Component\Mail;
use Shopsys\FrameworkBundle\Model\Mail\MailTemplate;
use Shopsys\FrameworkBundle\Model\Mail\MessageData;
use Shopsys\FrameworkBundle\Model\Mail\MessageFactoryInterface;
use Shopsys\FrameworkBundle\Model\Mail\Setting\MailSetting;
class PasswordChangedMail implements MessageFactoryInterface
{
// unique identifier of email template
public const MAIL_TEMPLATE_NAME = 'password_changed';
/**
* @var \Shopsys\FrameworkBundle\Component\Setting\Setting
*/
private $setting;
/**
* @param \Shopsys\FrameworkBundle\Component\Setting\Setting $setting
*/
public function __construct(Setting $setting)
{
$this->setting = $setting;
}
/**
* @param \Shopsys\FrameworkBundle\Model\Mail\MailTemplate $template
* @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser
*
* @return \Shopsys\FrameworkBundle\Model\Mail\MessageData
*/
public function createMessage(MailTemplate $template, $customerUser): MessageData
{
return new MessageData(
$customerUser->getEmail(),
$template->getBccEmail(),
$template->getBody(),
$template->getSubject(),
$this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL, $customerUser->getDomainId()),
$this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL_NAME, $customerUser->getDomainId())
);
}
}
Add new email template into data fixtures¶
To be able to test the email template, we need to populate the database with some data.
To do that we can just create a new record in src/DataFixtures/Demo/MailTemplateDataFixture.php
// class App\DataFixtures\Demo\MailTemplateDataFixture
/**
* @param \Doctrine\Common\Persistence\ObjectManager $manager
*/
public function load(ObjectManager $manager)
{
$mailTemplateData = $this->mailTemplateDataFactory->create();
$mailTemplateData->sendMail = true;
foreach ($this->domain->getAll() as $domainConfig) {
// ... already existing templates
+ $mailTemplateData->subject = t('Your password has changed');
+ $mailTemplateData->body = t('Dear {fullname},<br/><br/>
+ We wanted to let you know that your password has changed.
+ <br/><br/>
+ If you did not perform this action, you can recover access by entering {email} into the form at {password_reset_url}
+ <br/><br/>
+ Best regards
+ ', [], 'dataFixtures', $locale);
+
+ $this->createMailTemplate($manager, PasswordChangedMail::MAIL_TEMPLATE_NAME, $mailTemplateData, $domainId);
You can see we used several variable placeholders in this template ({fullname}
, {email}
, and {password_reset_url}
).
Right now they are treated as plain text.
We will allow to replace them with real values in the next step.
Note
In the example above we translated email subject and body. Don't forget to dump translations.
Replacing variables with values¶
We want to be able to use several variables and replace them with real values when the email should be sent.
To do that, we update previously created PasswordChangedMail
class.
First, we add constants representing each variable to be able to reference them in code.
class PasswordChangedMail implements MessageFactoryInterface
{
// unique identifier of email template
public const MAIL_TEMPLATE_NAME = 'password_changed';
+ public const VARIABLE_FULLNAME = '{fullname}';
+ public const VARIABLE_EMAIL = '{email}';
+ public const VARIABLE_PASSWORD_RESET_URL = '{password_reset_url}';
Replacing variables is internally supported in MessageData
class we have in our PasswordChangedMail
.
We just need to pass an array of replacements (in a format {variable} => realValue
).
// class App\Component\Mail\PasswordChangedMail
// DomainRouterFactory is necessary to be able to generate url to reset password form
+ /**
+ * @var \Shopsys\FrameworkBundle\Component\Router\DomainRouterFactory
+ */
+ protected $domainRouterFactory;
/**
* @param \Shopsys\FrameworkBundle\Component\Setting\Setting $setting
+ * @param \Shopsys\FrameworkBundle\Component\Router\DomainRouterFactory $domainRouterFactory
*/
- public function __construct(Setting $setting)
+ public function __construct(Setting $setting, DomainRouterFactory $domainRouterFactory)
{
$this->setting = $setting;
+ $this->domainRouterFactory = $domainRouterFactory;
}
public function createMessage(MailTemplate $template, $customerUser): MessageData
{
return new MessageData(
$customerUser->getEmail(),
$template->getBccEmail(),
$template->getBody(),
$template->getSubject(),
$this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL, $customerUser->getDomainId()),
$this->setting->getForDomain(MailSetting::MAIN_ADMIN_MAIL_NAME, $customerUser->getDomainId())
+ $this->getBodyVariablesReplacements($customerUser),
+ $this->getSubjectVariablesReplacements($customerUser)
);
}
and corresponding methods can look like this
// class App\Component\Mail\PasswordChangedMail
/**
* @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser
* @return array
*/
private function getSubjectVariablesReplacements(CustomerUser $customerUser): array
{
return [
self::VARIABLE_FULLNAME => htmlspecialchars($customerUser->getFullName(), ENT_QUOTES),
];
}
/**
* @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser
* @return array
*/
private function getBodyVariablesReplacements(CustomerUser $customerUser): array
{
$router = $this->domainRouterFactory->getRouter($customerUser->getDomainId());
return [
self::VARIABLE_FULLNAME => htmlspecialchars($customerUser->getFullName(), ENT_QUOTES),
self::VARIABLE_EMAIL => htmlspecialchars($customerUser->getEmail(), ENT_QUOTES),
self::VARIABLE_PASSWORD_RESET_URL => $router->generate('front_registration_reset_password', [], UrlGeneratorInterface::ABSOLUTE_URL),
];
}
Note
In this example, we're intentionally replacing all defined variables in the email body, but in the subject, only customer full name is replaced.
Warning
Replacements (real values) for the variables are most of the time some user-entered values. It's crucial to properly escape these values!
Sending email¶
Now when the template is stored in the database and we are properly replacing variables, we are ready to send this email when the user enters a new password after reset password process.
To make things easy, we add sending email directly into CustomerPasswordController::setNewPasswordAction()
.
In your application, you may want to consider a better place.
// class App\Controller\Front\CustomerPasswordController
/*
* @param \Symfony\Component\HttpFoundation\Request $request
+ * @param \App\Component\Mail\PasswordChangedMail $passwordChangedMail
+ * @param \Shopsys\FrameworkBundle\Model\Mail\MailTemplateFacade $mailTemplateFacade
+ * @param \Shopsys\FrameworkBundle\Model\Mail\Mailer $mailer
+ * @param \Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade $uploadedFileFacade
+ * @return \Symfony\Component\HttpFoundation\Response
*/
public function setNewPasswordAction(
Request $request,
+ PasswordChangedMail $passwordChangedMail,
+ MailTemplateFacade $mailTemplateFacade,
+ Mailer $mailer,
+ UploadedFileFacade $uploadedFileFacade
+ ) {
// ...
if ($form->isSubmitted() && $form->isValid()) {
$formData = $form->getData();
$newPassword = $formData['newPassword'];
try {
$customerUser = $this->customerUserPasswordFacade->setNewPassword($email, $this->domain->getId(), $hash, $newPassword);
+ $mailTemplate = $mailTemplateFacade->get(PasswordChangedMail::MAIL_TEMPLATE_NAME, $customerUser->getDomainId());
+ $messageData = $passwordChangedMail->createMessage($mailTemplate, $customerUser);
+ $messageData->attachments = $uploadedFileFacade->getUploadedFilesByEntity($mailTemplate);
+ $mailer->send($messageData);
And you need to register previously created class PasswordChangedMail
into your services.yaml
file as it should be autowired.
# config/services.yaml
services:
# ...
+ App\Component\Mail\PasswordChangedMail: ~
Go ahead and try to reset a customer password. You will receive email notification about the changed password for the account.
Make the mail template configurable in administration¶
One of the requirements was to be able to edit the template in administration. Let's make it possible.
Shopsys Framework made this task really easy.
You just need to define variables, their labels for the form, allowed usage, and whether they are required or not.
This configuration is made in PHP to ease translating values. We create new provider class.
declare(strict_types=1);
namespace App\Component\Mail;
use Shopsys\FrameworkBundle\Model\Mail\MailTemplateVariables;
class PasswordChangedMailTemplateVariablesProvider
{
public function create(): MailTemplateVariables
{
// first argument is Mail Template readable name
$mailTemplateVariables = new MailTemplateVariables(t('Password was changed'));
$mailTemplateVariables->addVariable(
PasswordChangedMail::VARIABLE_EMAIL, // reuse already defined variable placeholders
t('Customer email'), // readable name of the variable
MailTemplateVariables::CONTEXT_BODY, // variable takes place in body only
MailTemplateVariables::REQUIRED_BODY // variable is required in body
);
$mailTemplateVariables->addVariable(
PasswordChangedMail::VARIABLE_PASSWORD_RESET_URL,
t('Reset password link'),
MailTemplateVariables::CONTEXT_BODY
);
// by default, the variable is not required and can be used in both subject and body
$mailTemplateVariables->addVariable(
PasswordChangedMail::VARIABLE_FULLNAME,
t('Customer full name')
);
return $mailTemplateVariables;
}
}
Each variable is added with the addVariable(string $variable, string $label, $context, $required)
method.
$variable
is variable placeholder$label
is readable name to describe the meaning of the variable to the user$context
defines where the variable is applicable and can have one of these values:MailTemplateVariables::CONTEXT_BOTH
– variable can take place in the subject and body (default)MailTemplateVariables::CONTEXT_BODY
- variable can take place in the body onlyMailTemplateVariables::CONTEXT_SUBJECT
- variable can take place in the subject only
$required
defines where the variable is required and can have one of these values:MailTemplateVariables::REQUIRED_NOWHERE
- variable is optional (default)MailTemplateVariables::REQUIRED_BOTH
- variable have to be present in the body and in the subjectMailTemplateVariables::REQUIRED_BODY
- variable have to be present in the bodyMailTemplateVariables::REQUIRED_SUBJECT
- variable have to be present in the subject
When we have the variables ready, the last step is to register variables with proper mail template.
This can be done in config/services.yaml
file
Shopsys\FrameworkBundle\Model\Mail\MailTemplateConfiguration:
calls:
- method: addMailTemplateVariables
arguments:
- !php/const App\Component\Mail\PasswordChangedMail::MAIL_TEMPLATE_NAME
- '@=service("App\\Component\\Mail\\PasswordChangedMailTemplateVariablesProvider").create()'
Conclusion¶
Now in your database is a new email template and email from this template is sent to the user whenever he resets his password. This template can be easily changed from the administration