Backend Development

Creating Dynamically DB Stored Emails in Laravel and Livewire Part 2

Creating Dynamically DB Stored Emails in Laravel and Livewire

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.

 

 

<< Back to the first part

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:

mailtrap-demo-inbox

Now click on the settings -> SMTP Settings tab -> integrations -> choose laravel like so:

mailtrap-integration-laravel

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:

Creating Dynamically DB Stored Emails in Laravel 8 and Livewire - test email

Creating Dynamically DB Stored Emails in Laravel 8 and Livewire - test send email 2

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:

Creating Dynamically DB Stored Emails in Laravel - template parser

So according to this diagram the parser must implement these steps:

  1. Search and find all the template variables in the template string.
  2. If there is any template variables the parser must iterate over all the matched variables and query each variable using the variable key.
  3. 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)
  4. 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.
  5. 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

Creating Dynamically DB Stored Emails in Laravel - creating mail variables

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

Creating Dynamically DB Stored Emails in Laravel - sample mail template 2

  • Welcome message

Creating Dynamically DB Stored Emails in Laravel - sample mail template 1

  • Invitation message

Creating Dynamically DB Stored Emails in Laravel - sample mail template 3

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>&nbsp;</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>&nbsp;</p>
</div>
</body>
</html>
  • Invitation message
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div style="text-align: center;">[WEBSITE_LOGO]</div>
<p>&nbsp;</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>&nbsp;</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:

Creating Dynamically DB Stored Emails in Laravel - mailtrap test email

To test the other routes go to http://localhost/dynamic_mailable/public/send-welcome and http://localhost/dynamic_mailable/public/send-invitation respectively.

 

Download Source Code

5 1 vote
Article Rating

What's your reaction?

Excited
1
Happy
5
Not Sure
1
Confused
2

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments