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 ; public $user; public $token; public function __construct (User $user) { $this ->user = $user; $this ->token = Password::getRepository()->create($user); } 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); } 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:

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.