Every time I start a new project in Laravel with a starter kit, I enable the `MustVerifyEmail` feature. And I always take for granted that it works. The Laravel team does a great job making sure it works. But, recently, I asked myself, how does it actually work? That's what I'm going to explain in this article.
I'm going to explain to you by doing my own `MustVerifyPhoneNumber` feature. Let's get started.
The first thing is to create an interface called `MustVerifyPhone`. It is pretty similar to `MustVerifyEmail`. Below, you'll find the content of it:
interface MustVerifyPhoneNumber
{
/**
* Determine if the user has verified their phone number.
*/
public function hasVerifiedPhoneNumber(): bool;
/**
* Mark the given user's phone number as verified.
*/
public function markPhoneNumberAsVerified(): bool;
/**
* Send the phone number verification notification.
*/
public function sendPhoneNumberVerificationNotification(): void;
/**
* Get the phone number that should be used for verification.
*/
public function getPhoneNumberForVerification(): string;
}
Now, we need to implement this interface in your model, in this case, the `User` model:
class User extends Authenticatable implements MustVerifyEmail, MustVerifyPhoneNumber
{
...
}
`MustVerifyEmail` actually implements the required methods in a trait. But wait, no trait implements the interface in the `User` model, right? Well, the trait is on the `Authenticatable` base class. If you go to the base class, you'll see a `MustVerifyEmail` trait. Let's do something similar.
I created a `MustVerifyPhoneNumber` trait with the following content:
trait MustVerifyPhoneNumber
{
public function hasVerifiedPhoneNumber(): bool
{
return ! is_null($this->phone_verified_at);
}
public function markPhoneNumberAsVerified(): bool
{
return $this->forceFill([
'phone_verified_at' => $this->freshTimestamp(),
])->save();
}
public function sendPhoneNumberVerificationNotification(): void
{
$this->notify(new VerifyPhoneNumberNotification);
}
public function getPhoneNumberForVerification(): string
{
return $this->phone_number;
}
}
As you can see, there are two new columns we need to add to the `users` table: `phone_number` and `phone_verified_at`. We also need to create `VerifyPhoneNumberNotification`. Here is an example of the notification. Of course, you can create your own version:
class VerifyPhoneNumberNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct() {}
/**
* @return string[]
*/
public function via(User $notifiable): array
{
return [WhatsappChannel::class];
}
public function toWhatsapp(User $notifiable): TemplateMessage
{
$url = $this->verificationUrl($notifiable);
return (new TemplateMessage)
->name('verify_phone_number_v1')
->addUrlButton($url);
}
protected function verificationUrl(User $notifiable): string
{
return URL::temporarySignedRoute(
'verification.verify_phone',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getPhoneNumberForVerification()),
]
);
}
}
You can create your own custom channel to send the notification if you want. However, the part I want you to examine is the `verificationUrl` method. See how we generate a temporary signed route that we are going to send to the user. I took this from the `Illuminate\Auth\Notifications\VerifyEmail` notification. You can send the user to a controller to verify the phone number and finally redirect to your dashboard.
The next step is to override the `verified` middleware. This middleware ensures that the user is redirected to the verification page if the user is not verified. Let's create our own version of the middleware.
class EnsureUserIsValidatedMiddleware
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, ?string $redirectToRoute = null): Response|RedirectResponse
{
if (
$this->emailNotVerified(request()->user())
|| $this->phoneNumberNotVerified(request()->user())
) {
return Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
return $next($request);
}
private function emailNotVerified(?User $user): bool
{
return ! $user?->hasVerifiedEmail();
}
private function phoneNumberNotVerified(?User $user): bool
{
return ! $user?->hasVerifiedPhoneNumber();
}
}
In my case, I verify that both the email and phone number are verified; if not, we redirect to the `verification.notice` route. You can find this route in the `auth.php` routes file. Also, you can check the default `verified` middleware in `Illuminate\Auth\Middleware\EnsureEmailIsVerified`.
Finally, we just need to override the `verified` middleware in `bootstrap/app.php` like this:
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'verified' => EnsureUserIsValidatedMiddleware::class,
]);
...
})->create();
And this is how you can implement your own verification feature based on the `MustVerifyEmail` feature from Laravel.
Great article! 👏🏻 This was really helpful. I'm keen to read more about clean development practices using Laravel.