Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

Notice: the following article takes into consideration Laravel versions lower than 5.5. After that it is easier to use custom validation rules

Introduction

Laravel provides different approaches to validate your application’s incoming data.

My favorite method to validate input data in controllers is to create Form Requests: custom request classes that contain authorization and validation logic. They allow you to keep your controllers as clean as possible, because the logic is executed before hitting them.

However I always struggled with one simple requirement: showing a custom dynamic error message based on the failed validation of the request.

Let me please explain with a real example.

Requirements

While working on a complex web application I had to implement a page to allow the administrator to write his own ElasticSearch synonym rules.

How synonyms work in ElasticSearch is definitely out of the scope of this article, however let’s just keep in mind that you can submit synonyms, during the ElasticSearch index creation, in 2 ways:

  • using an external configuration file (defined by the synonyms_path)

PUT /test_index

{
    "settings": {
        "index" : {
            "analysis" : {
                "analyzer" : {
                    "synonym" : {
                        "tokenizer" : "whitespace",
                        "filter" : ["synonym"]
                    }
                },
                "filter" : {
                    "synonym" : {
                        "type" : "synonym",
                        "synonyms_path" : "analysis/synonym.txt"
                    }
                }
            }
        }
    }
}
  • or directly defining the mapping in the index settings (note the use of synonyms instead of synonyms_path)

PUT /test_index

{
    "settings": {
        "index" : {
            "analysis" : {
                "analyzer" : {
                    "synonym" : {
                        "tokenizer" : "whitespace",
                        "filter" : ["synonym"]
                    }
                },
                "filter" : {
                    "synonym" : {
                        "type" : "synonym",
                        "synonyms" : [
                            "i-pod, i pod => ipod",
                            "universe, cosmos"
                        ]
                    }
                }
            }
        }
    }
}

The format of the synonyms mapping is the same in both cases. An example, taken directly from the documentation, is defined as follows:

# Blank lines and lines starting with pound are comments.

# Explicit mappings match any token sequence on the LHS of "=>"
# and replace with all alternatives on the RHS.  These types of mappings
# ignore the expand parameter in the schema.
# Examples:
i-pod, i pod => ipod,
sea biscuit, sea biscit => seabiscuit

# Equivalent synonyms may be separated with commas and give
# no explicit mapping.  In this case the mapping behavior will
# be taken from the expand parameter in the schema.  This allows
# the same synonym file to be used in different synonym handling strategies.
# Examples:
ipod, i-pod, i pod
foozball, foosball
universe, cosmos
lol, laughing out loud

# If expand==true, "ipod, i-pod, i pod" is equivalent
# to the explicit mapping:
ipod, i-pod, i pod => ipod, i-pod, i pod
# If expand==false, "ipod, i-pod, i pod" is equivalent
# to the explicit mapping:
ipod, i-pod, i pod => ipod

# Multiple synonym mapping entries are merged.
foo => foo bar
foo => baz
# is equivalent to
foo => foo bar, baz

Of course, this format has to follow specific rules. For example you cannot have more than one arrow symbol (=>) per line. We could have been stricter about it and build a more complex and fancy UI to avoid confusion and errors, but given that the page is available only to administrators we decided to use a simple textarea as input field. Choosing this solution however forced us to consider validation as a mandatory requirement, to ensure the validity of the input format.

Custom Form Request

The first step was to create a custom Form Request, in order to validate the ElasticSearch format in a separate class and not directly in the controller.

Creating a new form request is straightforward because we can leverage an Artisan command:

> php artisan make:request ElasticsearchSynonymsRequest

And here’s the code:

<?php

class ElasticsearchSynonymsRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return Auth::user()->isAdministrator();
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'synonyms' => 'elasticsearch_synonyms'
        ];
    }
}

As you can see we are applying a custom validation rule on the synonyms field. This custom rule doesn’t exist in our Laravel application yet. We have to add it.

Custom validation Rule

Custom validation rules are usually defined in a custom service provider, using an extension by leveraging the extend method on the Validator facade.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;

class ValidatorServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('elasticsearch_synonyms', 'App\Validators\ElasticsearchSynonymValidator@validate');
    }
}

As you can see, the validation logic is delegated to a separate class, which is cleaner and also makes testing easier. However remember that for smaller rules you can always implement the code using a Closure.

Let’s see how the validate function is structured:

<?php

class ElasticsearchSynonymValidator
{
    /**
     * Validate function.
     *
     * @param $attribute
     * @param $value
     * @param $parameters
     * @param $validator
     * @return bool
     */
    public function validate($attribute, $value, $parameters, $validator)
    {
        $lines = explode("\n", $value);

        $errors = $this->validateLines($lines);

        if (empty(array_filter($errors))) {
            return true;
        }

        return false;
    }
}

There’s not much to break down here. The $value of the input textarea is exploded into an array and each individual line is validated indipendently. The $errors variable then contains an array that carries any error messages for the lines that failed the validation. If such array is empty we can safely say that the synonyms are valid.

Custom error message

Now comes the tricky part. How can we return a dynamic message, based on the validation performed in the validate method, back to the user?

For example, given the following input for the ElasticSearch synonyms field:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Blank lines and lines starting with pound are comments.

i-pod => i pod => ipod

foozball, foosball
lol, laughing out loud

# Multiple synonym mapping entries are merged.
foo => foo bar
foo => baz,,

we would like to inform the user that there are validation errors on lines 3 and 10, caused respectively by multiple mappings on the same line and trailing commas.

Unfortunately most Laravel tutorials available online show how to return a custom message, but unfortunately this message is completely detached by the validation logic, because it’s defined ahead of the validation. In your response you will only know if the input field is valid or not.

For example, in our Form Request the error message can be customized as follows

<?php

/**
 * Get the error messages for the defined validation rules.
 *
 * @return array
 */
public function messages()
{
    return [
        'synonyms.elasticsearch_synonyms' => 'The format of synonyms is invalid'
    ];
}

As you can see, using the format {field}.{rule} => {message} we can return a specific error message to the user. Unfortunately this doesn’t help the user to identify which lines of the input actually didn’t pass the validation.

Solving this issue is possible, but it’s a little bit tricky, especially given the simplicity of the Laravel framework.

We can create a replacer, a method that is usually used to replace placeholders in the static validation messages. We can leverage this method to provide a completely custom error message, modeled around the validation failures.

The replacer can be defined in the same service provider where we defined the custom validation rule (ValidatorServiceProvider):

<?php
/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    [...]

    Validator::replacer('elasticsearch_synonyms', 'App\Validators\ElasticsearchSynonymValidator@replace');
}

And here’s the updated content of the ElasticsearchSynonymValidator class:

<?php

class ElasticsearchSynonymValidator
{
    /**
     * Validate function.
     *
     * @param $attribute
     * @param $value
     * @param $parameters
     * @param $validator
     * @return bool
     */
    public function validate($attribute, $value, $parameters, $validator)
    {
        $lines = explode("\n", $value);

        $errors = $this->validateLines($lines);

        if (empty(array_filter($errors))) {
            return true;
        }

        return false;
    }

    /**
     * Replacer function.
     *
     * @param $message
     * @param $attribute
     * @param $rule
     * @param $parameters
     * @return bool
     */
    public function replace($message, $attribute, $rule, $parameters)
    {
        // what do we do here?
    }
}

So how can we share the content of the $errors variable, which gives an indication of the faulty lines of the input, using the replace function? Initially I tried adding a class property to share the value, but it didn’t work. What actually worked was adding the static keyword to this property:

<?php

class ElasticsearchSynonymValidator
{
    static $errors = [];

    public function validate($attribute, $value, $parameters, $validator)
    {
        $lines = explode("\n", $value);

        $errors = $this->validateLines($lines);

        if (empty(array_filter($errors))) {
            return true;
        }

        self::$errors = $errors;

        return false;
    }

    public function replace($message, $attribute, $rule, $parameters)
    {
        return implode("\n", self::$errors);
    }
}

The replacer is called only when the validation fails. Since the function has to return a string, we have to implode (using the line feed charachter) all the errors returned in our $errors array.

Showing the error message

As we said, all the errors found in the synonyms field are combined into a single big line. Inspecting the errors from the Laravel session (using the awesome Laravel Debugbar) shows this structure:

Illuminate\Support\ViewErrorBag {#1258
  #bags: array:1 [
    "default" => Illuminate\Support\MessageBag {#1257
      #messages: array:1 [
        "synonyms" => array:1 [
          0 => """
            Error on line 3 - Cannot define multiple mapping in single line (mom, mother => mommy, mum => mama, momma\r)\n
            Error on line 20 - Invalid mapping. Check for spaces, and trailing commas. (cloud, clouds,,\r)
            """
        ]
      ]
      #format: ":message"
    }
  ]
}

Of course this is not really optimal because the error is presented to the user on one single line. What can we do to present this error message to the user in a friendly manner?

We can leverage the formatErrors method, available inside the Form Request, and convert the single error from the default MessageBag into multiple errors, by exploding the original message. Here’s the implementation:

<?php

/**
 * Format the error message.
 *
 * @param Validator $validator
 * @return array
 */
protected function formatErrors(Validator $validator)
{
    $errors = $validator->getMessageBag()->get('synonyms');

    $bag = new MessageBag();

    foreach (explode("\n", $errors[0]) as $i => $error) {
        $bag->add('synonyms.' . $i, $error);
    }

    return $bag->toArray();
}

That’s it! Hope you find this content useful.

comments powered by Disqus

Remove Laravel Mix during tests

Introduction

Laravel Mix is a handy Webpack wrapper for frontend asset building pipelines. It definitely helps any Laravel …