trandafili.com

Just Some Notes In A Programming Blog

Testing Jargon in your validation process

4 months ago · 4 MIN READ
#laravel  #php  #tdd 

With the Laravel 5.5 release there have been some major improvements in the validation process. An important one is that now from the validator you can receive back only the data that passed validation.

To give a quick example.

if the input takes this form:


 $input = [ 'one' => 'two', 'three' => 'four'];

and then you validate the following way


$filtered = $request->validate([
                 'one' => 'required',
 ]);

$filtered will contain:


array:2 [
  "one" => "two"
]

That's all fine and dandy but what happens when you need to receive content that is a bit more convoluted?

Let's assume we have a scenario where you need to receive a Json object in your request.

Creatively we call this field 'json'.

Basically what happens when your request input looks like this?


 $input = [ 'one' => 'two', 'three' => 'four',  'json' => json_encode([
            'foo' => 'bar',
            'bad_stuff' => 'ping',
        ])];

First, how to validate in the basic sense such an object? And then how to filter out content we do not want to store from that object? And more than that, how to check if it contains certain fields while we make sure we remove the unnecessary ones?

Let's tackle these matters one by one.

Testing basic stuff. Say if this field is required:

Ok, well this would do, right?


$filtered = $request->validate([
            'one' => 'required',
            'json' => 'required',
        ]);

Now the harder questions.

In this scenario we also need to:

  1. Make sure we filter out unnecessary fields
  2. Make sure that our json object contains fields we mandatorily need it to

Well this seems a bit much for the standard validation flow.

Luckily L5.5 allows for custom validation rules.

To create a custom rule with some logic of our own, as described in the documentation https://laravel.com/docs/5.5/validation#custom-validation-rules we can can use artisan:


php artisan make:rule ValidateJsonObj

This will create a class as follows


<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ValidateJsonObj implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return strtoupper($value) === $value;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

This way our validation logic in the controller becomes:


$filtered = $request->validate([
            'one' => ['required'],
            'json' => ['required', new ValidateJsonObj],
        ]);

In the passes() function of the ValidateJsonObj we can now define our passing logic.

We said first we need to weed out the neccessary fields and keep only the good ones. Ps. Now we probably want to make this a generic reusable meccanism so we can use it in other scenarios, thus let's work with that in mind.

Assuming we define a json structure of the 'good stuff', the validation would take the following form

$goodJsonStuff = ['foo'];

        $filtered = $request->validate([
            'one' => ['required'],
            'json' => ['required', new ValidateJsonObj($goodJsonStuff)],
        ]);

which makes us able to accept the data in the constructor of the rule


class ValidateJsonObj implements Rule
{
    protected $whiteList;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct($whiteList)
    {
        $this->whiteList = $whiteList;
    }
    Now to the good part. 

    Filtering the parts that we want:

    ```

    public function passes($attribute, $value)
{
    $data = collect(json_decode($value, true))->only($this->whiteList);

             request()->request->set($attribute, json_encode($data));

}

    ```

$attribute is the name of the field we're validating. In this case 'json'. Whilst $value is the content of that field. Using the awesome tools in collection toolset, we turn the json first into an array, then into a collection where we can use only() to get only the portions we need.

Like so :

        $data = collect(json_decode($value, true))->only($this->whiteList);

That's cool but the request still contains the unwanted data right? True, so we write our cleaned up data back to the request, which then in turn will get returned from the validator.

                 request()->request->set($attribute, json_encode($data));

With this we have made sure that the result given back from the validator becomes the following:

array:2 [
  "one" => "two"
  "json" => "{"foo":"bar"}"
]

The final and most daunting task is now asserting that the data we want is actually contained in our json object. Asserting? That sounds like testing jargon, right?

A few days ago i stumbled upon this tweet from Caleb Porzio.

Basically the idea here is to use TestResponse to craft a new response that we can test against. It sounds a little vague but it goes like this:


$objectAsResponse = TestResponse::fromBaseResponse((new Response)->setContent($someJsonData));

$objectAsResponse->assertJsonFragment($someDataWeWantInTheJsonObject);

Quick note. assertJsonFragment() if it fails will throw a ExpectationFailedException exception.

We can use this as a result for our validator. After all, if it's thrown, we must fail the assertion.

Okay, let's put it all together.


public function passes($attribute, $value)
    {
                // get only the fields we want from the json object
        $data = collect(json_decode($value, true))->only($this->whiteList);

                // craft a response we can test against
        $objectAsResponse = TestResponse::fromBaseResponse((new Response)->setContent($data));

        try {

                        // try to see if the data we want is present in the json body
            $objectAsResponse->assertJsonFragment($this->whiteList);

                        // if so write our clean data to the request for the validator to return
            request()->request->set($attribute, json_encode($data));

                        // Make the validator pass
            return true;

        } catch (ExpectationFailedException $e) {

                        // Let the world burn
            return false;

        }
    }

Ultimately this will make our custom rule take the following form:


<?php

namespace App\Rules;

use Illuminate\Http\Response;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Foundation\Testing\TestResponse;
use PHPUnit\Framework\ExpectationFailedException;

class ValidateJsonObj implements Rule
{
    protected $whiteList;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct($whiteList)
    {
        $this->whiteList = $whiteList;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $data = collect(json_decode($value, true))->only($this->whiteList);

        $objectAsResponse = TestResponse::fromBaseResponse((new Response)->setContent($data));

        try {
            $objectAsResponse->assertJsonFragment($this->whiteList);
            request()->request->set($attribute, json_encode($data));
            return true;
        } catch (ExpectationFailedException $e) {
            return false;
        }
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute value is invalid.';
    }
}

You may use it like this:


$goodJsonStuff = ['foo'];

        $filtered = $request->validate([
            'one' => ['required'],
            'json' => ['required', new ValidateJsonObj($goodJsonStuff)],
        ]);

The rule will turn the following input:


         $input = [ 'one' => 'two',  'json' => json_encode([
          'foo' => 'bar',
          'bad_stuff' => 'ping',
       ])];

Into this:


$input = [ 'one' => 'two',  'json' => json_encode([
         'foo' => 'bar',
 ])];

That's all.

Happy coding. :)

···

Sidrit Trandafili

Jack of some trades, developer, music, literature and movie addict, enthusiast and early adopter of all things tech and innovative. Also beer.

comments powered by Disqus


Proudly powered by Canvas · Sign In