Zubair Mohsin

How email validation works in Laravel using `egulias/email-validation` package

It’s obvious from the name of this package that it helps with email address validation. It helps us make sure if the given email address is valid (according to some standards) or not.

Laravel has a powerful Validation engine which helps you validate your data on incoming requests. It also provides several ways to perform validation:

  • By calling validate method on Request object inside controller method
  • By calling $this->validate() method inside controller method
  • By using a separate FormRequest class
  • $this->valdiate() method is made available using ValidatesRequests trait on controller.

We'll follow the FormRequest based validation as it is becoming a standard in Laravel community.

Code Dive

Let's say we are validating an email address field like below inside a FormRequest:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UsersRequest extends FormRequest
{
    public function rules()
    {
        return [
            'email_address' => ['email']
        ];
    }
}

And then use UsersRequest inside a controller like below:

class UsersController
{
    public function store(UsersRequest $request)
    {
        User::create($request->validated());
    }
}

What happens when we send the request?

When we send the request to above controller method, a service provider named FormRequestServiceProvider is booted ( along with other service providers ).

Let's take a look at its boot method:

namespace Illuminate\Foundation\Providers;

use Illuminate\Contracts\Validation\ValidatesWhenResolved;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Routing\Redirector;
use Illuminate\Support\ServiceProvider;

class FormRequestServiceProvider extends ServiceProvider
{
    public function boot()
    {
        *$this->app->afterResolving(ValidatesWhenResolved::class,
                function ($resolved) {
            $resolved->validateResolved();
        });*

        $this->app->resolving(FormRequest::class, function ($request, $app) {
            $request = FormRequest::createFrom($app['request'], $request);

            $request->setContainer($app)
                ->setRedirector($app->make(Redirector::class));
        });
    }
}

If we interpret the highlighted code block, it says:

After resolving the class which implements ValidatesWhenResolved interface, call the validateResolved method on its instance.

Our UsersRequest class extends the FormRequest class which implements ValidatesWhenResolved interface. Implementation for validateResolved method has been added to a Trait which can be found at Illuminate\Foundation\Http\ValidatesWhenResolvedTrait which is given below:

public function validateResolved()
{
    $this->prepareForValidation();

    if (!$this->passesAuthorization()) {
        $this->failedAuthorization();
    }

    **$instance = $this->getValidatorInstance();**

    **if ($instance->fails()) {
        $this->failedValidation($instance);
    }**

    $this->passedValidation();
}

getValidatorInstance() method has been overridden inside FormRequest class like below:

/**
 * Get the validator instance for the request.
 *
 * @return \Illuminate\Contracts\Validation\Validator
*/
protected function getValidatorInstance()
{
    if ($this->validator) {
        return $this->validator;
    }

    $factory = $this->container->make(ValidationFactory::class);

    if (method_exists($this, 'validator')) {
        $validator = $this->container->call(
            [$this, 'validator'], compact('factory')
        );
    } else {
        $validator = $this->createDefaultValidator($factory);
    }

    if (method_exists($this, 'withValidator')) {
        $this->withValidator($validator);
    }

    $this->setValidator($validator);

    return $this->validator;
}

/**
 * Create the default validator instance.
 *
 * @param  \Illuminate\Contracts\Validation\Factory  $factory
 * @return \Illuminate\Contracts\Validation\Validator
*/
protected function createDefaultValidator(ValidationFactory $factory)
{
    return $factory->make(
        $this->validationData(),
        $this->container->call([$this, 'rules']),
        $this->messages(),
        $this->attributes()
    )->stopOnFirstFailure($this->stopOnFirstFailure);
}

For the ease of understanding, above code can be translated to:

If developer has provided their own implementation of validator using validator() method on UsersRequest class, use that Validator. Otherwise, createDefaultValidator and provide data to be validated, rules and messages etc. to Validator instance.

Now that a Validator instance has been created, next in validateResolved() method is calling fails() method the given instance.

namespace Illuminate\Validation;

class Validator implements ValidatorContract
{
    public function fails()
    {
        return ! $this->passes();
    }

        //ripped off implementation of passes() method
        public function passes()
    {
        foreach ($this->rules as $attribute => $rules) {
            **foreach ($rules as $rule) {
                $this->validateAttribute($attribute, $rule);
            }**
        }
    }

    //ripped off implementation of validateAttribute() method
    protected function validateAttribute($attribute, $rule)
    {
        $this->currentRule = $rule;

        [$rule, $parameters] = ValidationRuleParser::parse($rule);

        if ($rule == '') {
            return;
        }

        $value = $this->getValue($attribute);

        $validatable = $this->isValidatable($rule, $attribute, $value);

        **$method = "validate{$rule}";**

        if ($validatable &&! **$this->$method($attribute, $value, $parameters, $this)**)
        {
            $this->addFailure($attribute, $rule, $parameters);
        }
    }

}

fails() method calls for passes() method which in turn calls validateAttribute() method. This is where our data attributes are checked against the validation rules we provided.

validate{$rule} string is creating a method name for the given rule. Since we are exploring email validation rule, it will become validateEmail() . Next, this method is being called as part of if condition.

Since implementations of all available validation methods can be found in Illuminate\Validation\Concerns\ValidatesAttributes trait. Hence the validateEmail() method below:

namespace Illuminate\Validation\Concerns;

use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use Egulias\EmailValidator\Validation\SpoofCheckValidation;

trait ValidatesAttributes
{

        /**
     * Validate that an attribute is a valid e-mail address.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @param  array  $parameters
     * @return bool
     */
    public function validateEmail($attribute, $value, $parameters)
    {
        $validations = collect($parameters)
            ->unique()
            ->map(function ($validation) {
                if ($validation === 'rfc') {
                    return new RFCValidation();
                } elseif ($validation === 'strict') {
                    return new NoRFCWarningsValidation();
                } elseif ($validation === 'dns') {
                    return new DNSCheckValidation();
                } elseif ($validation === 'spoof') {
                    return new SpoofCheckValidation();
                } elseif ($validation === 'filter') {
                    return new FilterEmailValidation();
                } elseif ($validation === 'filter_unicode') {
                    return FilterEmailValidation::unicode();
                } elseif (is_string($validation) && class_exists($validation)) {
                    return $this->container->make($validation);
                }
            })
            ->values()
            ->all() ?: [new RFCValidation()];

        return (new EmailValidator)->isValid($value, new MultipleValidationWithAnd($validations));
    }}

This is how Laravel validates an email address by leveraging various email validation classes from egulias/email-validation package.

Interesting facts about this package in Laravel context

  • This package was first used by Laravel in v5.8
  • Pull Request to add this functionality was opened by Leo Sjöberg and merged by Taylor Otwell in Nov, 2018
  • PR can be found here
  • Before the addition of this package, Laravel used filter_var function of PHP to validate email address.

I hope you enjoyed this post. Next, we will see how Laravel uses league/commonmark package. You can follow me on Twitter or join my newsletter to keep yourself updated.