Scout APM is PHP application performance monitoring designed for developers. With tracing logic that ties issues back to the line of code causing them, you can pinpoint n+1 queries, memory leaks, and other abnormalities in real time so you can knock them out and get back to building a great product. Start your free 14-day trial today and get the performance insight you need in less than 4 minutes.

Sending a welcome mail with Laravel 5.3

Original – by Freek Van der Herten – 7 minute read

Recently I was working an a project where, in order to use the webapp, users should first apply for an account. Potential users can fill in request form. After the request is approved by an admin they may use the app.

Our client expected that the barrier to request an account should be very low. That's why the request form doesn't contain a password field. Instead, when an account is approved, a welcome mail is sent with a link to where the user can specify a password.

In this post I'd like to show you how we solved this with Laravel 5.3's mailables.

High level

This is what we're going to do. Whenever an admin approves a user we're going to fire off a UserApproved event. We're going to listen for that event and, when we hear it, we'll generate a password reset token and send a welcome mail. The welcome mail contains a link to a screen where a user can choose a password.

Show me the code!

The user model contains a method to approve the user. Whenever a user is approved the UserApproved event is sent.

public function approve(): User
{
    $this->status = 'approved'
    $this->save();

    event(new UserApproved($this));

    return $this;
}

You could opt to, instead of firing of an event, just send a mail directly from your controller. That'll work, and for smallish projects that's perfectly fine in my book. But I prefer, when the application should send out a couple of different mails, to fire off events. We can than listen for those events in a specialized EventHandler. The code of that event handler shows which mails are being sent of when.

Here's the code of the event handler used in our project:

namespace App\Mail;

use App\Events\UserApproved;
use App\Events\UserRefused;
use App\Events\UserRegistered;
use Illuminate\Contracts\Events\Dispatcher;
use Mail;

class EventHandler
{
    public function subscribe(Dispatcher $events)
    {
        $events->listen(UserRegistered::class, function (UserRegistered $event) {
            Mail::send(new RegistrationReceived($event->user));
            Mail::send(new UserWaitingForApproval($event->user));
        });

        $events->listen(UserApproved::class, function(UserApproved $event) {
            Mail::send(new Welcome($event->user));
        });

        $events->listen(UserRefused::class, function(UserRefused $event) {
            Mail::send(new Refusal($event->user));
        });
    }
}

In this EventHandler we can clearly see which mails are being sent when. If you like this approach and would like to use it as well, don't forget to register the EventHandler in the EventServiceProvider.

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as IlluminateEventServiceProvider;

class EventServiceProvider extends IlluminateEventServiceProvider
{
    protected $listen = [];

    protected $subscribe = [
        \App\Mail\Eventhandler::class,
    ];
}

Let's take a closer look at the App\Mail\Welcome-class. It's a mailable: a class responsible for configuring and sending a mail message. Head over to the Laravel docs on mailables to learn more.

This welcome mail should contain a link to a screen where a user can pick a password. That functionality reminds me very much of a password reset. A password reset mail also contains a link to such a screen.

A mailable is, in my opinion, the perfect place to put some extra code that should be executed when the mail is going to be sent. In our case we can manually generate a reset token. This is the code to do that:

Password::getRepository()->create($user);

If we put that line inside our mailable a reset token will be generated when the mail is sent. Here's the full code of App\Mail\Welcome:

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Password;

class Welcome extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    /** @var \App\Models\User */
    public $user;

    /** @var string */
    public $token;

    /**
     * @param \App\Models\User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;

        $this->token = Password::getRepository()->create($user);
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this
            ->to($this->user->email)
            ->subject('Welcome to '.config('app.name'))
            ->view('mails.member.welcome');
    }
}

Any public property on the mailable is going to be accessible by the view. So the view mails.member.welcome has access to the $user and the newly generated $token

Let's take a look at the mail-view. config('auth.passwords.users.expire') determines when password reset token will expire. In a vanilla Laravel app is set to 60 minutes. In my project I've set this to a higher value.

This is entire contents of that mail.member.welcome view:

@extends('mails._layouts.master')

@section('content')
    <h1>Welcome to <a href="{{ config('app.url') }}">{{ config('app.name') }}</a></h1>
    <p>
        Dear {{ $user->first_name }},
    </p>
    <p>
        Your account has been approved. You can now pick a password at our site and login.
    </p>
    <table>
        <tr>
            <td>
                <p>
                    <a href="{{ action('WelcomeController@index', [$token]) }}" class="btn-primary">
                       Pick a password
                    </a>
                </p>
            </td>
        </tr>
    </table>

    <p><em>This link is valid until {{ Carbon\Carbon::now()->addMinutes(config('auth.passwords.users.expire'))->format('Y/m/d') }}.</em></p>
@endsection

The App\Http\Controllers\WelcomeController will handle the click on the link in the sent welcome mail. If the link is valid it will display a form where the user can pick a password. When that valid form is submitted, the password will be saved, the user will be logged in an redirected to the member section.

These are the routes that have been set up for the WelcomeController.

Route::group(['middleware' => 'guest'], function() {
    Route::get('welcome/{token}', 'WelcomeController@index');
    Route::post('welcome/save-password', 'WelcomeController@savePassword');
});

This is the code of the controller itself. It uses the ResetsPasswords trait provided by Laravel. It contains many methods that make it easy to reset a password. Notice that I've wrapped my own savePassword method around the reset method. My own method does nothing extra but I thought savePassword is more clear than the generic reset in this context.

namespace App\Http\Controllers;

use Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Password;

class WelcomeController extends Controller
{
    use ResetsPasswords;

    public function index(Request $request, string $token = null)
    {
        if (! $user = User::findByToken($token)) {
            flash()->error('The link you clicked is invalid.');

            return redirect()->to('/login');
        };

        return view('welcome')->with([
            'token' => $token,
            'email' => $request->email,
            'user' => $user
        ]);
    }

    public function savePassword(Request $request)
    {
        return $this->reset($request);
    }

    /**
     * Get the response for a successful password reset.
     */
    protected function sendResetResponse(string $response): Response
    {
        flash()->info('Welcome! You are now logged in! Your password was saved.');

        return redirect('/member-home');
    }
}

This is the findByToken method that's defined on the User model:

/**
 * @param string $token
 *
 * @return \App\Services\Auth\User|null
 */
public static function findByToken(string $token)
{
    $resetRecord = app('db')->table('password_resets')->where('token', $token)->first();

    if (empty($resetRecord)) {
        return;
    }

    return static::where('email', $resetRecord->email)->first();
}

To finish things off here's the code for the welcome-view.

@extends('_layouts.main')

@section('title', 'welcome')

@section('content')

    <h1>Welcome</h1>

    {!! Form::open(['action' => 'WelcomeController@savePassword']) !!}

    {!! Form::hidden('token', $token) !!}
    {!! Form::hidden('email', $user->email) !!}

    <div>
       {!! Form::label('password', 'Password', ['class' => 'label--required'] ) !!}
       {!! Form::password('password', null, ['autofocus' ]) !!}
    </div>

    <div>
       {!! Form::label('password_confirmation', 'Confirm password', ['class' => 'label--required']) !!}
       {!! Form::password('password_confirmation', [null]) !!}
       {!! Html::error($errors->first('password')) !!}
    </div>

    <div>
       {!! Form::button('Save password', ['type'=>'submit']) !!}
    </div>

    {!! Form::close() !!}

@endsection

Though there's quite some code involved in making all of this work, it's fairly easy to set up. If you have any questions, or suggestions for improving this workflow, let me know in the comments below.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

Webmentions

Jino Antony liked on 13th November 2019
Nishit Nayak liked on 13th November 2019
Roger Pence liked on 13th November 2019
Murali ⌘ Krishna liked on 12th November 2019
Florian Wartner liked on 12th November 2019
Peter Brinck ? liked on 12th November 2019
Kristoffer liked on 12th November 2019
Kristoffer retweeted on 12th November 2019
Andrés Herrera García liked on 12th November 2019
Wyatt liked on 12th November 2019
Freek Van der Herten replied on 12th November 2019
Signed urls are an excellent idea, I'll change that
Joseph Silber replied on 12th November 2019
Why use tokens over signed URLs?
Tom Witkowski replied on 12th November 2019
Since when is the prefix back-slash needed? ? '\\'.WelcomeController::class And how about accepting an array of options to create a route group? This would allow to assign middlewares without a custom group around the macro.
Tom Witkowski replied on 12th November 2019
Shouldn't the "invalidLinkResponse" come with a 4xx status code? ?
Freek Van der Herten replied on 12th November 2019
Yeah, I'll change that
Gareth Thompson ?? replied on 12th November 2019
Only that you guys are AMAZING and this is yet another package that we'll make use of! Thanks to you and your team!
Freek Van der Herten replied on 12th November 2019
I should test if it's really needed but I think this it is needed for people that will use this macro in a group that has a namespace set up.
Dave Sebek liked on 12th November 2019
Dave Sebek replied on 12th November 2019
You are a BEAST!
Joseph Leedy ?? liked on 12th November 2019
Julio Serpone liked on 12th November 2019
Tally Schmeits liked on 12th November 2019
Xavier Seguí liked on 12th November 2019
etenzy liked on 12th November 2019
K ? AMA21 liked on 12th November 2019
Ruddy liked on 12th November 2019
bschen liked on 12th November 2019
Remco liked on 12th November 2019
Rodrigo Pedra Brum liked on 12th November 2019
64 Robots liked on 12th November 2019
Abbah Anoh liked on 12th November 2019
Yanuar S. Wibowo liked on 12th November 2019
Stefan Bauer replied on 12th November 2019
Perfect timing. Thought two days ago about such a package ;D
PHP Synopsis retweeted on 12th November 2019
DominikGeimer liked on 12th November 2019
Oliver Davies liked on 12th November 2019
Theta2 liked on 12th November 2019
Miguel Piedrafita ? liked on 12th November 2019
Dries Vints retweeted on 12th November 2019
Dries Vints replied on 12th November 2019
?
Dries Vints liked on 12th November 2019
NUNO MADURO ➕ liked on 12th November 2019
Haneef Ansari liked on 12th November 2019
shafiullah Ramaki liked on 12th November 2019
Manuel Regidor liked on 12th November 2019
Povilas Korop replied on 12th November 2019
Oh right markdownmail.com - it's not exactly what I meant, this is Markdown. I thought it was more for clients to manage templates themselves, like Mailchimp editor with minimum functionality.
Freek Van der Herten replied on 12th November 2019
I think @bobbybouwmann once built a design generator
Jimmy Lipham liked on 12th November 2019
Anne Koep liked on 12th November 2019
Povilas Korop replied on 12th November 2019
I was thinking there's a need in the market for mail customizations in Laravel, in general, so we build Notifications and clients then want to change texts, add more templates if you want, change design etc. And I think no one actually did it for Laravel, or did I miss something?
Vincenzo La Rosa liked on 12th November 2019
Chea Bonnak liked on 12th November 2019