PHP - - By Younes Rafie

Validating your data with Respect Validation

Validation is an important aspect of every application’s interaction with data. Instead of reinventing the wheel every time, the community collaborated on some useful packages like Symfony, Laravel, Zend, etc. In this article, we’re going to introduce a lesser known package called Respect Validation, which provides some nice new features. Let’s get started.

When using a framework, there is a great chance that you have a validation component shipped with it. The Symfony Validator Component provides a set of standard validation rules that you’ll need for your application.

class UserSubscriptionForm
{
    protected $email;

    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint('email', new \Assert\Email([
            'message' => 'Invalid email.'
        ]));
    }
}

I’m sure your subscription form contains more than just an email, but let’s keep it simple. Inside your controller you’ll have to trigger the validation like so:

public function store(Request $request){
    $userSubscriptionForm = UserSubscriptionForm::fromInput($request->all());
    $validator = $this->get('validator');
    $errors = $validator->validate($userSubscriptionForm);

    if (count($errors) > 0) {
        // redirect with errors
    }
    
    // return success
}

After creating a new UserSubscriptionForm from the user input, we will trigger the validation and collect the validation errors if we found any.

Laravel also ships with the Illuminate validation package. It does the same job, but in a different way. Let’s use our previous validation example and convert it to Laravel.

class UserSubscriptionController extends Controller
{
    public function store(Request $request)
    {
        $validator = \Validator::make(
            [
                'email' => $request->get('email')
            ],
            [
                'email' => 'required|email'
            ]
        );
    
        if($validator->fails()){
            $errors = $validator->messages();
    
            // redirect with errors
        }
        
        // return success
    }
}

The Zend validator package is not much different from the others. For the same previous example, we can do the following:

class UserSubscriptionController extends Controller
{
    public function store(Request $request)
    {
        $validator = new Zend\Validator\EmailAddress();

        if(!$validator->isValid($request->get('email'))){
            $errors = $validator->getMessages();

            // redirect with errors
        }

        // return success
    }
}

I’m sure you’re already familiar with at least one of the packages mentioned above. In this article, we are going to introduce Respect/Validation and we will highlight the main differences from the other packages.

Respect Validation

The Respect validator introduces a simple way of validating your data. Let’s start by completing the same previous example.

class UserSubscriptionController extends Controller
{
    public function store(Request $request)
    {
        $emailValidator = \Respect\Validation\Validator::email();

        if (!$emailValidator->validate($email)) {
            // redirect with errors
        }

        // return success
    }
}

So, nothing new! The first thing you’ll notice is that we didn’t retrieve the list of errors after a validation failure. To retrieve the list of errors, we need to use the assert method which throws an exception containing the list of error messages.

class UserSubscriptionController extends Controller
{
    public function store(Request $request)
    {
        $emailValidator = \Respect\Validation\Validator::email();

       try{
           $emailValidator->assert($request->get('email'));
       }
       catch(\Respect\Validation\Exceptions\NestedValidationExceptionInterface $ex){
           $errors = $ex->getMainMessage();
           // redirect with errors
       }

        // return success
    }
}

We also have the ability to add multiple rules to the same value by chaining methods.

Validator::string()->noWhitespace()->length(4, 10);
Validator::numeric()->between(5, 10);
Validator::date()->between(5, 10);

Another way to validate multiple rules is to use the allOf rule which accepts a list of rules.

$inputValidator = \Respect\Validation\Validator::allOf(
    new String(),
    new Length(4, 10)
);

You probably need to validate some data to match at least one of your business rules. Let’s take the login form as an example where the user can either enter their email address or username. The username must be alpha numeric and between 4 and 16 characters long. You can check the documentation for more details about the list of available rules.

$usernameOrEmailValidator = \Respect\Validation\Validator::oneOf(
    new \Respect\Validation\Rules\Email(),
    \Respect\Validation\Validator::string()->alnum()->noWhitespace()->length(4, 16)
);

One feature that you don’t often see in validation packages is the rule negation functionality. You can specify the rules that you don’t want to match, for example.

$inputValidator = \Respect\Validation\Validator::not(\Respect\Validation\Validator::numeric());

Custom Error Messages

As mentioned earlier, when an assertion exception is thrown, you can get error messages using one of the following methods.

  • getFullMessage: returns a general error message with a list of the failing rules. Asserting Validator::email()->assert('notAValidEmail') will throw the following message.
\-These rules must pass for "notAValidEmail"
  \-"notAValidEmail" must be valid email
  • getMainMessage: returns a general error message without specifying the failing rules. The email example returns These rules must pass for "notAValidEmail.
  • findMessages: accepts an array as a parameter containing the list of messages for the failing rules.
$this->messages = [
    'alpha'                 => '{{name}} must only contain alphabetic characters.',
    'alnum'                 => '{{name}} must only contain alpha numeric characters and dashes.',
    'numeric'               => '{{name}} must only contain numeric characters.',
    'noWhitespace'          => '{{name}} must not contain white spaces.',
    'length'                => '{{name}} must length between {{minValue}} and {{maxValue}}.',
    'email'                 => 'Please make sure you typed a correct email address.',
    'creditCard'            => 'Please make sure you typed a valid card number.',
    'date'                  => 'Make sure you typed a valid date for the {{name}} ({{format}}).',
    'password_confirmation' => 'Password confirmation doesn\'t match.'
];

// inside the try catch block

try {
    $this->rules[$input]->assert($value);
} catch (\Respect\Validation\Exceptions\NestedValidationExceptionInterface $ex) {
    dump($ex->findMessages($this->messages));
}

This will return a custom message if one the failing rules exists inside our messages array. You can read more about custom messages in the documentation.

To make things a little more practical, we will be validating a user subscription form. The form contains a set of fields to identify the user, and a small section for the billing info.

User Subscription Validation

I will be using Laravel for my example, but I think it doesn’t matter in this case since we don’t use any Laravel magic. You can start by adding the package using composer: composer require respect/validation. The user subscription form will look like this:

User Subscription Form

// resources/views/home.blade.php

<form action="/send" method="POST">
    <legend>Profile</legend>

    <input class="form-control" name="username" type="text" autofocus="" value="{{\Input::old('username')}}">

    <input class="form-control" name="email" type="email" value="{{\Input::old('email')}}">

    <input class="form-control" name="password" type="password" value="">

    <input class="form-control" name="password_confirmation" type="password" >
            
    <legend>Billing</legend>

    <input class='form-control' size='4' type='text' name="cardHolderName" value="{{\Input::old('cardHolderName')}}">

    <input class='form-control' size='20' type='text' name="cardNumber" value="{{\Input::old('cardNumber')}}">

    <input class='form-control' size='20' type='text' name="billingAddress" value="{{\Input::old('billingAddress')}}">

    <input class='form-control' size='4' type='text' name="cvc" value="{{\Input::old('cvc')}}">

    <input class='form-control' size='2' type='text' name="expirationMonth" value="{{\Input::old('expirationMonth')}}">

    <input class='form-control' size='4' type='text' name="expirationYear" value="{{\Input::old('expirationYear')}}">

    <button class="btn btn-primary">Join</button>
</form>
// app/Http/routes.php

Route::get('/', 'UserSubscriptionController@index');
Route::post('/send', 'UserSubscriptionController@send');

Our UserSubscriptionController class will have two methods. One for printing the form and another for processing the form submission. We will create another separate class to process the form validation.

// app/Http/Controllers/UserSubscriptionController

class UserSubscriptionController extends Controller
{
    protected $userSubscription;

    public function __construct(UserSubscriptionValidator $userSubscription)
    {
        $this->userSubscription = $userSubscription;
    }

    public function index()
    {
        return view('home');
    }

    public function send(Request $request, Redirector $redirector)
    {
        $inputs = $request->all();
        $isValid = $this->userSubscription->assert($inputs);

        if (!$isValid) {
            return $redirector->back()->withInput()->with('errors', $this->userSubscription->errors());
        }

        return "Everything is Good!";
    }
}

The App\Validation\UserSubscriptionValidator class will be exposed through two methods (assert and errors).

// app/Validation/UserSubscriptionValidator.php

use \Respect\Validation\Validator as V;

class UserSubscriptionValidator
{
    /**
     * List of constraints
     *
     * @var array
     */
    protected $rules = [];
    
    /**
     * List of customized messages
     *
     * @var array
     */
    protected $messages = [];

    /**
     * List of returned errors in case of a failing assertion
     *
     * @var array
     */
    protected $errors = [];

    /**
     * Just another constructor
     *
     * @return void
     */
    public function __construct()
    {
        $this->initRules();
        $this->initMessages();
    }
    
    /**
     * Set the user subscription constraints
     *
     * @return void
     */
    public function initRules()
    {
        $dateFormat = 'd-m-Y';
        $now = (new \DateTime())->format($dateFormat);
        $tenYears = (new \DateTime('+10 years'))->format($dateFormat);

        $this->rules['username'] = V::alnum('_')->noWhitespace()->length(4, 20)->setName('Username');
        $this->rules['password'] = V::alnum()->noWhitespace()->length(4, 20)->setName('password');
        $this->rules['email'] = V::email();
        $this->rules['cardHolderName'] = V::alpha()->setName('Card holder name');
        $this->rules['cardNumber'] = V::creditCard()->setName('card number');
        $this->rules['billingAddress'] = V::string()->length(6)->setName('billing address');
        $this->rules['cvc'] = V::numeric()->length(3, 4)->setName('CVC');
        $this->rules['expirationDate'] = V::date($dateFormat)->between($now, $tenYears)->setName('expiration date');
    }
}

Our constructor calls the initRules method which sets the validation constraints. The setName method value is injected into the error messages template. The initMessages method sets the list of customized messages; this can be moved outside the class if you’re going to use them again for another form.

// app/Validation/UserSubscriptionValidator.php

/**
 * Set user custom error messages
 *
 * @return void
 */
public function initMessages()
{
    $this->messages = [
        'alpha'                 => '{{name}} must only contain alphabetic characters.',
        'alnum'                 => '{{name}} must only contain alpha numeric characters and dashes.',
        'numeric'               => '{{name}} must only contain numeric characters.',
        'noWhitespace'          => '{{name}} must not contain white spaces.',
        'length'                => '{{name}} must length between {{minValue}} and {{maxValue}}.',
        'email'                 => 'Please make sure you typed a correct email address.',
        'creditCard'            => 'Please make sure you typed a valid card number.',
        'date'                  => 'Make sure you typed a valid date for the {{name}} ({{format}}).',
        'password_confirmation' => 'Password confirmation doesn\'t match.'
    ];
}

The {{name}} tag will be updated with the corresponding name passed previously using the setName method. Some rules contain more variables, like {{minValue}} and {{maxValue}} for the length rule. Same thing for the date rule. The next step is to define our assert method to perform the actual validation.

/**
 * Assert validation rules.
 *
 * @param array $inputs
 *   The inputs to validate.
 * @return boolean
 *   True on success; otherwise, false.
 */
public function assert(array $inputs)
{
    $expirationMonth = array_get($inputs, 'expirationMonth');
    $expirationYear = array_get($inputs, 'expirationYear');
    $inputs['expirationDate'] = '01-' . $expirationMonth . '-' . $expirationYear;

    foreach ($this->rules as $rule => $validator) {
        try {
            $validator->assert(array_get($inputs, $rule));
        } catch (\Respect\Validation\Exceptions\NestedValidationExceptionInterface $ex) {
            $this->errors = $ex->findMessages($this->messages);
            return false;
        }
    }

    $passwordConfirmed = $this->assertPasswordConfirmation($inputs);

    return $passwordConfirmed;
}

The first step is to create a valid date from the expiration month and year. This will help us to easily validate it against actual date values. We loop through the list of validation rules and test if the user input matches the defined rule. If an assertion fails, we set the errors message attribute and return false to the controller.

Currently, the package doesn’t provide a rule to automaticaly validate password confirmation. The assertPasswordConfirmation method will only test if the passed values are equal and set the error message in case of failure.

public function assertPasswordConfirmation(array $inputs)
{
    $passwordConfirmation = array_get($inputs, 'password_confirmation');
    if ($inputs['password'] !== $passwordConfirmation) {
        $this->errors['password_confirmation'] = $this->messages['password_confirmation'];
        return false;
    } else {
        return true;
    }
}

The last part is to return the errors to the user if it fails, and we also need to update our home template to print errors in case of failure.

public function errors()
{
    return $this->errors;
}
// home.blade.php

@if(\Session::has('errors'))
    <div class="alert alert-danger">
        @foreach(\Session::get('errors') as $error)
            @if(!empty($error))
                <p>{{$error}}</p>
            @endif
        @endforeach
    </div>
@endif

Validation test

Creating Your Own Rules

You can create your own business rules and use the with method to add your namespace to the lookup array. You can check the documentation for more details about creating your own constraints.

Respect\Validation\Validator::with('App\\Validation\\User\\Rules\\');

If you found that the rules you’re trying to use exist in a Zend or Symfony validation package, you can directly use them using one of the following methods.

Respect\Validation\Validator::sf('Language')->validate('Arabic');
Respect\Validation\Validator::zend('EmailAddress')->validate('me@gmail.com');

Conclusion

This article briefly introduced the Respect validation package. We saw how simple the API is and how it integrates with other validators like Symfony and Zend. It’s also easy to extend and add new rules to the validation process. If you’ve never used it before, I really encourage you to give it a try. If you’ve already used it in some of your projects, I would really love to know what you think about it and how it fits your use case in the long term – let us know in the comments below!

Sponsors
Login or Create Account to Comment
Login Create Account