DEV Community

Cover image for Laravel - validate 'true' or 'false' as boolean
Nour-Eddine ECH-CHEBABY
Nour-Eddine ECH-CHEBABY

Posted on • Originally published at echebaby.com

Laravel - validate 'true' or 'false' as boolean

This post was originally published on my personal blog on 30th December 2021.

Every once in a while you come across a situation where you need to validate a request input as a boolean, and the input value is 'true' or 'false' (notice that I wrapped the values inside single quotes to indicate that those are actually strings). You would expect, as I did, that this will just work out of the box and Laravel validator will just treat them as booleans. But, that's not the case, instead, you will be hit with this beautiful error message 'The inputName field must be true or false.'.

To deeply understand why this is happening, let's go through an example, let's say that we have a form request that we call PostRequest.

Form requests are custom request classes that encapsulate their own validation and authorization logic.

-- Laravel docs.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'is_published' => 'required|boolean',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

PostRequest, for simplicity purposes, has a single request input that, first, is required, and second, is boolean, and this input is named is_published. Let's also say that is_published value coming to us from the client is 'true' or 'false'.

At this point we already know that validation wouldn't pass. So, how can we handle this?

Beforehand

Before diving in, I want to show you the logic Laravel uses to validate booleans internally, you can actually see that in validateBoolean() method located in Illuminate/Validation/Concerns/ValidatesAttributes.

<?php

namespace Illuminate\Validation\Concerns;

/**
 * Validate that an attribute is a boolean.
 *
 * @param  string  $attribute
 * @param  mixed  $value
 * @return bool
 */
public function validateBoolean($attribute, $value)
{
    $acceptable = [true, false, 0, 1, '0', '1'];

    return in_array($value, $acceptable, true);
}
Enter fullscreen mode Exit fullscreen mode

We can also see that in this unit tests.

<?php

use Illuminate\Validation\Validator;

// ...

public function testValidateBoolean()
{
    $trans = $this->getIlluminateArrayTranslator();

    // ...

    $v = new Validator($trans, ['foo' => 'false'], ['foo' => 'Boolean']);

    $this->assertFalse($v->passes());

    $v = new Validator($trans, ['foo' => 'true'], ['foo' => 'Boolean']);

    $this->assertFalse($v->passes());

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Source : https://github.com/laravel/framework/blob/8.x/tests/Validation/ValidationValidatorTest.php

Solutions

The first approach - use prepareForValidation method

Let's start with what I think it's a more easy approach to implement, it's what Laravel calls "Preparing Input For Validation",
and that's done by using prepareForValidation() method. Like its name reveals, this method allows us to add new request inputs or update existing request inputs before going through the validation rules.

So in our small example here, we will try to convert the is_published value to an actual boolean, and merge it back to the original request.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class PostRequest extends FormRequest
{
    // ...

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'is_published' => 'required|boolean',
        ];
    }

    /**
     * Prepare inputs for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $this->merge([
            'is_published' => $this->toBoolean($this->is_published),
        ]);
    }

    /**
     * Convert to boolean
     *
     * @param $booleable
     * @return boolean
     */
    private function toBoolean($booleable)
    {
        return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
    }
}
Enter fullscreen mode Exit fullscreen mode

FILTER_VALIDATE_BOOLEAN tries to be smart, recognizing words like 'Yes', 'No', 'Off', 'On', 'true' and 'false', and is not case-sensitive when validating strings.

FILTER_VALIDATE_BOOLEAN returns true for '1', 'true', 'on' and 'yes'. Returns false otherwise.

When FILTER_NULL_ON_FAILURE flag is set, false is returned ONLY for '0', 'false', 'off', 'no', and '', and null is returned for all non-boolean values.

Understanding how FILTER_NULL_ON_FAILURE flag affect the filter_var function is essential, especially while tackling the second approach as we are going to see later on.

For that reason let me provide you with some examples to demonstrate how the toBoolean method behave under different use cases.

$this->toBoolean('1');                // true
$this->toBoolean('true');             // true
$this->toBoolean('on');               // true
$this->toBoolean('yes');              // true

$this->toBoolean('0');                // false
$this->toBoolean('false');            // false
$this->toBoolean('off');              // false
$this->toBoolean('no');               // false

$this->toBoolean('not a boolean');    // null
Enter fullscreen mode Exit fullscreen mode

Until here it makes perfect sense, "truthy" booleans are true, "falsy" booleans are false, others are just null. Perfect!

$this->toBoolean('');                 // false
Enter fullscreen mode Exit fullscreen mode

This, is where it gets interested, this last use case could really be confusing, I myself was waiting for null as a return value, but we get a boolean instead (false in this case).

This will cause a false validation, because the empty string will be valuated as a boolean, which make the validation passes.

Notice that this will never be the case in our example, because we have a required rule, if the request input (is_published) is an empty string, the validation will fail before even hitting the boolean rule.

I thought it was important to bring this up.

With that been said, let's jump right into the second approach.

The second approach - use a custom validation rule

While the first approach works perfectly, there is a "classy" way to validate the input as boolean, and that's by creating custom validation rules using rule objects.

Laravel provides a variety of helpful validation rules; however, you may wish to specify some of your own. One method of registering custom validation rules is using rule objects. To generate a new rule object, you may use the make:rule Artisan command.

-- Laravel docs.

Let's use this command to generate a rule that validates a string value of true and false as boolean.

php artisan make:rule Boolean
Enter fullscreen mode Exit fullscreen mode

Laravel will place the new rule in the app/Rules directory. If this directory does not exist, Laravel will create it when you execute the Artisan command to create your rule.

-- Laravel docs.

As promised, a Boolean class is created in app/Rules namespace, and here is what it looks like:

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class Boolean implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

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

Once the Boolean rule has been created, we are ready to define its behavior.

A rule object contains two methods: passes and message. The passes method receives the attribute value and name, and should return true or false depending on whether the attribute value is valid or not. The message method should return the validation error message that should be used when validation fails.

Small turn - make global helper functions

But, just before doing that, it may be useful to extract toBoolean from before to its own function and make it available globally.

An easy and efficient way of creating global functions in Laravel is to autoload it directly from Composer. The autoload section of composer accepts a files array that is automatically loaded.

  1. Create a helpers.php file wherever you like. I usualy keep my global helpers in app/Support/helpers.php.
  2. Add your your helper functions, no need to specify any namespace (so we don't have to use use function to call them).

    if (!function_exists('to_boolean')) {
    
        /**
         * Convert to boolean
         *
         * @param $booleable
         * @return boolean
         */
        function to_boolean($booleable)
        {
            return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
        }
    }
    
  3. In composer.json inside the autoload section add the following line "files": ["app/Support/helpers.php"].

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        },
        "files": ["app/Support/helpers.php"]
    }
    
  4. Run composer dump-autoload

Now our to_boolean function is callable anywhere in our project.

Back to our Boolean rule.

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

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

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return __('validation.boolean');
    }
}
Enter fullscreen mode Exit fullscreen mode

For our case we can safly remove the constructor.

The is_bool function is a native php function, it finds out whether a variable is a boolean.

The __ function is a Laravel strings helper, it translates the given translation string or translation key using your localization files.

It's almost finished, all we have to do now is update our PostRequest to implement the custom rule object Boolean like this:

<?php

use App\Rules\Boolean;

    // ...

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'is_published' => ['required', new Boolean],
        ];
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

Finally, our post has come to an end. So a quick recap, we have described two ways, approaches if you will, to validate 'true' and 'false' as boolean with Laravel validator.

The first approach is preparing input for validation throughout the use of prepareForValidation method provided to us by FormRequest.

The second approach is using custom validation rules, more precisely rule objects, for that we have created our own Boolean object to do the job.

I know that I said that the first approach is easier to implement, but now that I use the rule object more often, I find it to be simpler and cleaner, the abstraction in rule object is more "developer-friendly" so to speak, the first approach is, arguably, more verbose. Either way, it's good to know them both, use whatever suits your use case or your personal preference.

References





Originally published at https://echebaby.com on December 30, 2021.

Top comments (0)