In the previous part of the tutorial we handled the CRUD of the mail variables and mail templates, we will continue in this part by completing the whole circle.
Send Test Mail
You may wonder why we need to send a test email. In fact this is beneficial step to help preview the template in the email provider and it’s style and if the template needs style update.
At first we need to setup the email configuration in laravel to allow sending emails. We will use a third party service used for test email sending which is the mailtrap. So go to https://mailtrap.io and create a new account, after this mailtrap will create a demo inbox for you as in this figure:
Now click on the settings -> SMTP Settings tab -> integrations -> choose laravel like so:
Copy the settings in the the .env file email settings like so:
MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=<your username> MAIL_PASSWORD=<your password> MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=info@example.com MAIL_FROM_NAME="${APP_NAME}"
Run this command to clear the cache after updating .env:
php artisan cache:clear
Next we will create a laravel mailer class, this is a simple class that acts as a blueprint for the email sending functionality. The class extends Illuminate\Mail\Mailable and accepts two arguments in the constructor, the subject and body respectively.
app/Mail/SendMail.php
<?php namespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class SendMail extends Mailable { use Queueable, SerializesModels; public $emailSubject = ""; public $emailBody = ""; /** * Create a new message instance. * * @return void */ public function __construct($emailSubject, $emailBody) { $this->emailSubject = $emailSubject; $this->emailBody = $emailBody; } /** * Build the message. * * @return $this */ public function build() { $this->subject($this->emailSubject); return $this->view('emails.blueprint'); } }
resources/views/emails/blueprint.blade.php
{!! $emailBody !!}
Updating the Send Mail component
app/Http/Livewire/MailTemplate/SendMailTemplate.php
<?php namespace App\Http\Livewire\MailTemplate; use App\Mail\SendMail; use App\Models\MailTemplate; use Illuminate\Support\Facades\Mail; use Livewire\Component; class SendMailTemplate extends Component { public $showModal = false; public $templateId; public $email = ""; protected $listeners = ['showSendModal', 'closeCreateModal']; public function submit() { $mailTemplate = MailTemplate::find($this->templateId); Mail::to($this->email)->send(new SendMail($mailTemplate->subject, $mailTemplate->body)); session()->flash('success', 'Check your inbox for test message'); } public function showSendModal($id) { $this->showModal = true; $this->templateId = $id; } public function closeCreateModal() { $this->showModal = false; } public function render() { return view('livewire.mail-template.send-mail-template'); } }
resources/views/livewire/mail-template/send-mail-template.blade.php
<div> @if($showModal) <div class="modal fade show show-modal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <form method="post" wire:submit.prevent="submit"> @csrf <div class="modal-header"> <h5 class="modal-title">Send Test Message</h5> <button type="button" class="btn-close" wire:click="$emitTo('mail-template.send-mail-template', 'closeCreateModal')"></button> </div> <div class="modal-body"> @if (session()->has('success')) <div class="alert alert-success"> {{ session('success') }} </div> @endif <div class="form-group"> <label>Email</label> <input type="email" class="form-control" name="variable_value" wire:model.lazy="email" required /> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" wire:click="$emitTo('mail-template.send-mail-template', 'closeCreateModal')">Close</button> <button type="submit" class="btn btn-primary">Send</button> </div> </form> </div> </div> </div> @endif </div>
Refresh the browser and go to /mail-template. Then create a template and then click the Test Send button as shown in the figure:
After sending the email inspect the mailtrap inbox to see how the email template look like.
Mail Template Parser
To use the mail template we have implemented and stored in the DB in a real message such as a welcome email or a contact form, we need to create some sort of a parser that parses the mail variables inside the required template with their actual values in order for the template to be sent correctly.
We will implement a simple parser that will take the raw template and convert it according to this diagram:
So according to this diagram the parser must implement these steps:
- Search and find all the template variables in the template string.
- If there is any template variables the parser must iterate over all the matched variables and query each variable using the variable key.
- After returning the variable info from db the parser attempt the get the variable actual value depending on the variable type, for example if the variable in the form [INPUT:param] then the value will be fetched from laravel request()->input(param)
- After finishing iterating over the matched variables the parser will store the updated variables into a new array whose index is the variable key and the value is the variable actual value.
- The final step is that the parser will make a replacement of all occurrences of the variable keys in the template with the actual values and return the compiled string.Â
Now by understanding the above steps i created a sample parser class described below:
app/Lib/TemplateParser.php
<?php namespace App\Lib; use App\Models\MailVariable; use Illuminate\Support\Str; class TemplateParser { /** * @var $templateStr */ private $templateStr; /** * optional in case of raw messages * * @var mixed|string $rawMessage */ private $rawMessage = ""; /** * @var $templateVariables */ private $templateVariables = []; /** * The final compiled template * * @var string $compiled */ private $compiled = ""; /** * container for dynamic data variables * * @var array $dynamicData */ private $dynamicData = []; public function __construct($templateStr, $rawMessage = "") { $this->templateStr = $templateStr; $this->rawMessage = $rawMessage; } public function process() { // first: retrieve variable keys using regex using this pattern $matches = $this->getMatchedTemplateVariables(); // second: loop for the matches and retrieve each variable from mail_variables table if($matches && count($matches) > 0) { $this->templateVariables = $this->getParsedTemplateVariables($matches); } $this->compiled = $this->replaceKeysWithValues(); } public function getCompiled() { return $this->compiled; } /** * setter for dynamic data */ public function __set($propertyName, $propertyValue) { $this->dynamicData[$propertyName] = $propertyValue; } /** * getter for dynamic data */ public function __get($propertyName) { if(array_key_exists($propertyName, $this->dynamicData)) { return $this->dynamicData[$propertyName]; } return ""; } private function getMatchedTemplateVariables() { $regex = '/\[\w.+?\]/m'; preg_match_all($regex, $this->templateStr, $matches, PREG_SET_ORDER); return $matches; } private function getParsedTemplateVariables($matches) { $templateVariables = []; foreach ($matches as $match) { $mailVariable = MailVariable::where('variable_key', $match[0])->first(); if($mailVariable) { $templateVariables[$mailVariable->variable_key] = $this->getRealVariableValue($mailVariable->variable_key, $mailVariable->variable_value); } } return $templateVariables; } private function replaceKeysWithValues() { return str_replace(array_keys($this->templateVariables), array_values($this->templateVariables), $this->templateStr); } private function getRealVariableValue($variableKey, $variableValue) { // if variable value not empty return it if($variableValue) { return $variableValue; } // else look for this in the reserved variables below if(array_key_exists($variableKey, $this->reservedVariableKeys())) { return $this->reservedVariableKeys()[$variableKey]; } // else if the variable key is a form input if(Str::contains($variableKey, "INPUT")) { return $this->getInputTypeVariable($variableKey, $variableValue); } // else if the key is a dynamic data variable if(Str::contains($variableKey, "DYNAMIC")) { return $this->getDynamicTypeVariable($variableKey, $variableValue); } // otherwise return the value as is return $variableValue; } private function getInputTypeVariable($variableKey, $variableValue) { $inputName = explode(":", str_replace("]", "", str_replace("[", "", $variableKey)))[1]; if(request()->has($inputName)) { return request()->input($inputName); } return $variableValue; } private function getDynamicTypeVariable($variableKey, $variableValue) { $propertyName = explode(":", str_replace("]", "", str_replace("[", "", $variableKey)))[1]; if(isset($this->dynamicData[$propertyName])) { return $this->dynamicData[$propertyName]; } return $variableValue; } private function reservedVariableKeys() { // in my case i suppose i have these reserved variables return [ "[WEBSITE_LOGO]" => $this->getWebsiteLogo(), "[WEBSITE_NAME]" => config('app.name'), "[YEAR]" => date("Y"), "[MESSAGE_BODY]" => $this->rawMessage, "[CURR_USER]" => (auth()->check() ? auth()->user()->name : "") ]; } private function getWebsiteLogo() { // return whatever your website logo $logoUrl = 'https://example.com/public/logo.png'; return '<img src="'.$logoUrl.'" width="200" height="160" alt="website logo" />'; } }
Example usage:
$mailTemplate = MailTemplate::where('template_key', 'contact-us')->first(); $templateParser = (new TemplateParser($mailTemplate->body)); $templateParser->process(); echo $templateParser->getCompiled();
In the class i declared some helper properties:
- $templateStr: Represents the mail template string.
- $rawMessage: This property be default empty and used only in cases when to send specific email as a raw message.
- $templateVariables: Represents a list of email variables found in the template.
- $dynamicData: To set dynamic data if the template contains variables in this form: [DYNAMIC:param]
- $compiled: Contains the final template after it has compiled and parsed.
The class entry point starts from the process() method. The process() method first get the matched template variables by invoking getMatchedTemplateVariables() method. If there is a template variables then we store it inside the $templateVariables array property.
getMatchedTemplateVariables() extracts the template variable using regular expression and return the matches array.
getParsedTemplateVariables() returns an array of mail variables with the mail key as the array index and the variable value. The method do so by iterating over the matched variables and then invoking another method getRealVariableValue().
getRealVariableValue() return the variable value depending on the variable type. For example if the variable value is not empty then it’s value returned directly, if the variable exists in the reserved variables then we pull the value from the reserved variables array.
If the variable key is in the form [INPUT:param] then we fetch the value from laravel request()->input() method.
If the variable key is in the form [DYNAMIC:param] then we fetch the value from $dynamicData array property.
At the end of the process() method i invoke replaceKeysWithValues() method and assign it to the $compiled property, so now the $compiled property holds the converted template.
The __set() and __get() magic methods used to store dynamic into $this->dynamicData array. As you might guess these two magic methods called automatically when writing data or reading to inaccessible or nonexistent properties.
As a quick tip you can modify the reservedVariableKeys() according to your needs, for example you can remove the [WEBSITE_LOGO] and [WEB_NAME] and supply their values from the edit mail variables popup.
Applying Mail Template Parser
Now comes the moment to apply this parser to sample email. You can apply this parser in any place in the application you send email from, for example in console commands, after form submit, etc. So i will create a test controller to try this.
For our demonstration example try to add these variables like this photo
If you wish you can change [COPYRIGHT] and [WEBSITE_URL] values as per yours needs.
Then we will need three html email templates for our experiment, i will add the source code for them down below, look at these pictures and add them in the same way.
Note: when adding the html body into the rich text editor be sure to view the html source from the editor top view menu and check the template variables is entered correctly, for example if the template variable contains spaces it will not be parsed like:
[WEBSITE_URL ] // invalid [WEBSITE_URL] // valid
- Contact message
- Welcome message
- Invitation message
Html source code of each template
- Contact message
<!DOCTYPE html> <html> <head> </head> <body> <div style="text-align: center;">[WEBSITE_LOGO]</div> <p>Dear Admin,</p> <p>A new contact message from <strong>[WEBSITE_NAME]</strong> with the following details</p> <p><strong>Name:</strong> [INPUT:name]</p> <p><strong>Email:</strong> [INPUT:email]</p> <p><strong>Content:</strong> [INPUT:content]</p> <div style="text-align: center;"> <p><span class="badge bg-secondary">[COPYRIGHT] </span><span class="badge bg-secondary">[YEAR]</span></p> <p><span class="badge bg-secondary"><a href="[WEBSITE_URL]">[WEBSITE_URL]</a></span></p> <p> </p> </div> </body> </html>
- Welcome message
<!DOCTYPE html> <html> <head> </head> <body> <div style="text-align: center;">[WEBSITE_LOGO]</div> <p><em>Dear [DYNAMIC:username],</em></p> <p>Thanks for joining our platform</p> <p>Your account is created successfully. Please use the link below to login</p> <p><em>[LOGIN_URL]</em></p> <p>Your email address: <em>[DYNAMIC:email]</em></p> <p><em>Your password: choosen password</em></p> <p>Regards,</p> <div style="text-align: center;"> <p><span class="badge bg-secondary">[COPYRIGHT] </span><span class="badge bg-secondary">[YEAR]</span></p> <p><span class="badge bg-secondary"><a href="[WEBSITE_URL]">[WEBSITE_URL]</a></span></p> <p> </p> </div> </body> </html>
- Invitation message
<!DOCTYPE html> <html> <head> </head> <body> <div style="text-align: center;">[WEBSITE_LOGO]</div> <p> </p> <p>[MESSAGE_BODY]</p> <div style="text-align: center;"> <p><span class="badge bg-secondary">[COPYRIGHT] </span><span class="badge bg-secondary">[YEAR]</span></p> <p><span class="badge bg-secondary"><a href="[WEBSITE_URL]">[WEBSITE_URL]</a></span></p> <p> </p> </div> </body> </html>
After that create this test controller
app/Http/Controllers/SendEmailController.php
<?php namespace App\Http\Controllers; use App\Lib\TemplateParser; use App\Mail\SendMail; use App\Models\MailTemplate; use Illuminate\Http\Request; use Illuminate\Support\Facades\Mail; class SendEmailController extends Controller { public function showContactForm() { return view('contact'); } public function sendContact(Request $request) { $this->validate($request, [ 'name' => 'required', 'email' => 'required|email', 'content' => 'required|min:10' ]); $mailTemplate = MailTemplate::where('template_key', 'contact-us')->first(); if(!$mailTemplate) { session()->flash('error', 'Make sure that the required email template exist and try again'); return redirect()->back(); } $siteAdmin = env("MAIL_FROM_ADDRESS"); $templateParser = (new TemplateParser($mailTemplate->body)); $templateParser->process(); Mail::to($siteAdmin)->send(new SendMail($mailTemplate->subject, $templateParser->getCompiled())); session()->flash('success', 'Message is being sent'); return redirect()->back(); } public function sendWelcomeEmail() { $mailTemplate = MailTemplate::where('template_key', 'welcome-email')->first(); $templateParser = (new TemplateParser($mailTemplate->body)); $templateParser->username = auth()->user()->name; $templateParser->email = auth()->user()->email; $templateParser->process(); Mail::to(auth()->user()->email)->send(new SendMail($mailTemplate->subject, $templateParser->getCompiled())); } /** * using raw [MESSAGE_BODY] */ public function sendInvitation() { $mailTemplate = MailTemplate::where('template_key', 'invitation-request')->first(); $rawMessage = "<strong>Dear user,</strong><p>We would like to invite to join our platform, click on the below to proceed </p> <a href='#'>http://demo.example.com/invitation-accept</a> "; $templateParser = (new TemplateParser($mailTemplate->body, $rawMessage)); $templateParser->process(); Mail::to(auth()->user()->email)->send(new SendMail($mailTemplate->subject, $templateParser->getCompiled())); } }
This controller contains three actions for email testing:
- The first action for test email in a contact form
- The second action for test email for welcome message
- The third action for test email for invitation request.
For the contact form i have added a view in resources/views
resources/views/contact.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Contact Us</div> <div class="card-body"> @if (session('error')) <div class="alert alert-danger" role="alert"> {{ session('error') }} </div> @endif @if (session('success')) <div class="alert alert-success" role="alert"> {{ session('success') }} </div> @endif <form role="form" action="{{url('contact')}}" method="post"> @csrf <div class="controls"> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="form_name">Name *</label> <input id="form_name" type="text" name="name" class="form-control" placeholder="Please enter your name" required value="{{old('name')}}"> @error('name') <span class="invalid-feedback">{{$message}}</span> @enderror </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="form_email">Email *</label> <input id="form_email" type="email" name="email" class="form-control" placeholder="Please enter your email" required value="{{old('email')}}"> @error('email') <span class="invalid-feedback">{{$message}}</span> @enderror </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="form_content">Message *</label> <textarea id="form_content" name="content" class="form-control" placeholder="Write your message here." rows="4">{{old('content')}}</textarea> @error('content') <span class="invalid-feedback">{{$message}}</span> @enderror </div> </div> </div> <div class="form-group"> <input type="submit" class="btn btn-success pt-2 btn-block " value="Send Message"> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
Also update the routes to be like so:
routes/web.php
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); Route::get('/contact', [\App\Http\Controllers\SendEmailController::class, 'showContactForm']); Route::post('/contact', [\App\Http\Controllers\SendEmailController::class, 'sendContact']); Route::get('/send-welcome', [\App\Http\Controllers\SendEmailController::class, 'sendWelcomeEmail']); Route::get('/send-invitation', [\App\Http\Controllers\SendEmailController::class, 'sendInvitation']); Route::group(['middleware' => 'auth'] ,function () { Route::group(['prefix' => 'mail-variable'], function() { Route::get('/', App\Http\Livewire\MailVariable\ListMailVariable::class); }); Route::group(['prefix' => 'mail-template'], function() { Route::get('/', App\Http\Livewire\MailTemplate\ListMailTemplate::class); Route::get('/create', App\Http\Livewire\MailTemplate\CreateMailTemplate::class); Route::get('/edit/{id}', App\Http\Livewire\MailTemplate\EditMailTemplate::class); Route::get('/preview/{id}', function ($id) { $mailTemplate = \App\Models\MailTemplate::find($id); return $mailTemplate->body; }); }); });
To check this go to the contact route at http://localhost/dynamic_mailable/public/contact , fill the form and check your mailtrap inbox. I have tested this and this a screenshot in mailtrap:
To test the other routes go to http://localhost/dynamic_mailable/public/send-welcome and http://localhost/dynamic_mailable/public/send-invitation respectively.