mikebronner's avatar

Form Validation Combination of Fields Must Be Unique

This question has probably been asked before, and I'm just not using the right search terms, apologies if it has been answered.

I need to perform form validation where the combination of two form fields must be unique in the database.

Let's take the example of First Name and Last Name fields: I want John Doe to only be able to submit the form once, but John Smith and Jane Doe shouldn't be prevented from submitting the form if John Doe already exists.

0 likes
26 replies
mikebronner's avatar

@bobbybouwmann thanks for the reply and suggestions. I took a look at the package you recommended, and it seems to have quite a few issues since last year. I wouldn't mind the learning experience of going through writing a custom validator for this use case, if you're up for it (or can point me to a blog post that describes the methodology). :)

Thanks!

bobbybouwmann's avatar

Well I really should create a blog post for it, but I will type it out for you :P Give me a minute

3 likes
bobbybouwmann's avatar

Oke we need to create two files and we need to update one file.

Note that I use the namespace of App. If you changed the namespace or place it somewhere else you need to update the namespace of course ;)

Now let's start with step one, we will create a service provider for the this.

ValidationServiceProvider

Now that we know the name of the service provider, let's create the file and add some code. We create a new directory called validation in the services directory in the app directory. A lot of directories :P

app\Services\Validation\ValidationServiceProvider.php

<?php namespace App\Services\Validation;

use Illuminate\Support\ServiceProvider;

class ValidationServiceProvider extends ServiceProvider {


    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        // We don't have to register anything here so we keep this empty!
    }

    /**
     * Boot the service provider.
     */
    public function boot()
    {
        // Need to override the default validator with our own validator
        // We can do that by using the resolver function
        $this->app->validator->resolver(function ($translator, $data, $rules, $messages)
        {
            // This class will hold all our custom validations
            return new CustomValidation($translator, $data, $rules, $messages);
        });
    }

}

CustomValidation

Now our next step is to create the CustomValidation class. We can do that in the same directory as the service provider app\Services\Validation\CustomValidation.php

<?php namespace App\Services\Validation;

use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Validator;

// It's important to extend the Validator class
// By doing that we make sure we keep all the other rules as well!
// Now if you want to override a rule you can do that here as well
// For now we will only focus on creating rules
class CustomValidation extends Validator {

    // Laravel keeps a certain convention for rules
    // So the function is called validateGreaterThen    
    // Then the rule is greater_then

    // A validation rule accepts three parameters
    // $attribute This is the name of the input
    // $value This is the value of the input
    // $parameters This is a parameter for the rule, so greater_then:1,2 has two parameters
    // the $parameters are returned as an array so for the first parameter: $parameters[0]

    // Now that we know how a rule works let's create one
    /**
     * $attribute Input name
     * $value Input value
     * $parameters Table, field1
     */
    public function validateUniqueWith($attribute, $value, $parameters)
    {
        // Now that we have our data we can check for the data

        // We first grab the correct table which is passed to the function
        // Now we need to do some checking using Eloquent
        // If you don't understand this, please let me know
        $result = DB::table($parameters[0])->where(function($query) use ($attribute, $value, $parameters) {
            $query->where($attribute, '=', $value) // where firstname = value
                ->orWhere($parameters[1]), '=', $value); // where lastname = value
        })->first();

        // Now we check if we have a record
        // If we do have a record we return false and the validation will fail
        // If we can't find a record we return true and the validation will succeed
        return $result ? false : true;
    }

    // If you need more examples, let me know ;)
}

Register the service provider

Now we have a service provider with a custom validation class but Laravel doesn't know where to find it. We can simply update the providers array in the app config config\app.php

'providers' => [
    
    // other providers

    // Add the new provider at the bottom
    'App\Services\Validation\ValidationServiceProvider',    

],

All done

Now that we are done we can use the rule like this

$rules = [
    'firstname' => 'required|unique_with:users,lastname'
];

More information on service providers: http://laravel.com/docs/master/container

9 likes
mikebronner's avatar

Ah, I was thinking that might be the direction you were headed, it is basically built on a custom DB query, then evaluates and returns the results. :) Thanks for the detailed write-up! :) Will give this a shot and report back.

mikebronner's avatar

OK, this is a good start ... however, the core issue still pertains: the validator is only looking at a single value, not the combined value of both fields.

Basically, I need to check if first_name + last_name already exists, not just compare against first_name from the form, which is what $value holds. Perhaps validation of multiple fields cannot be done using the validator (if this is a real limitation of the validator, not just a perceived limitation by me)?

If all else fails, I could, of course, make an explicit check for this in the controller and return to form with old data and flash message. (But validator would be more elegant.)

bobbybouwmann's avatar

Can you give a use case with some good examples, then I have a better idea of what you want to achieve!

1 like
mikebronner's avatar

Actually, the example on the README for the package you suggested is exactly what I'm trying to achieve. I took a look at their code and how they are approaching the issue, and it takes some parsing and injecting of form data into the validation rule, and gets a bit complicated.

I gave their package a try and it worked. :)

Nonetheless, thank you very much for being so willing to help out! I now know how to build custom validators, which is more than I knew this morning :)

gvauditor's avatar

Hi,

Is this thread is alive? I'm new to laravel and stumble upon the same situation, I thing your talking about like generating a checking like this:

WHERE firstname = '$firstname' AND lastname = '$lastname'

Thank you.

pmall's avatar

Yes no need for custom validation rules for this :

$id = $this->id ?: 'NULL';

$rule = 'unique:users,first_name,' . $id . ',id,last_name,' . $this->last_name;
5 likes
Shantanu's avatar

@bobbybouwmann I want to use rules() as well as messages(). I have used unique_with in rules(). But then how do I check it in messages() ?

douglas_quaid's avatar

@bobbybouwmann This is a great answer, I did exactly what you said here but in the providers array I wrote this instead...

    /* custom validation */
        App\Services\Validation\ValidationServiceProvider::class,

Is this ok? Also, my validation isn't working when I do the following:

$validator = Validator::make($request->all(), [
            'test_field' => 'required|numeric|unique_with:table1,test_field2',
        'test_field2 => 'required|numeric,
        ]);

        if ($validator->fails()) {
            return $this->respondFailedParameters();
        }

Do you know why it's not working? Do I have to call it in the way you called it above?

$rules = [
    'firstname' => 'required|unique_with:users,lastname'
];

Do I do something like this to get the above equivalent?

try {
            $rules = [
                'test_field' => 'required|numeric|unique_with:table1,test_field2',
            'test_field2 => 'required|numeric,
            ];
        } catch (Exception $e) {
            return $this->respondFailedParameters();
        }

UPDATE: it seems to be working with the try catch block with the $rules array above, but I cannot catch any errors now? Do you know how can I catch these exceptions?

douglas_quaid's avatar

@bobbybouwmann any advice here how to use the $rules variable you mentioned? I'd like to catch the errors from the validator, but I can no longer use

$validator = Validator::make(...)
bobbybouwmann's avatar

Laravel has since my answer changed a bit. It will now throw an exception when the validation fails. See: https://github.com/laravel/framework/blob/5.4/src/Illuminate/Validation/Validator.php#L281

However you can catch this exception of course.

// In your controller

public function store(Request $request)
{
    $rules = []; // Put your rules here

    try {
        $this->validate($request, $rules
    } catch (\Illuminate\Validation\ValidationException $exception) {
        // Do something with the validator  
        // For example

        // Get the failed validation rules
        // Returns an array
        $errors = $exception->validator->failed();
    }
}

This is the work around for this. However Laravel is catching this exception automatically for you and creates a global $errors variable for you, which you can access in your view. If you have the \Illuminate\View\Middleware\ShareErrorsFromSession middleware enabled in your app\Http\Kernel.php middleware group.

If you want to do something else with this my example above would be an option.

douglas_quaid's avatar

@bobbybouwmann Thanks for your reply. I didn't see it until now because you didn't tag my username.

I will try this and let you know how it works. I'm using this custom validator in my API and will let you know if I have any questions. Thanks!

roerjo's avatar

@bobbybouwmann @douglas_quaid Couldn't you use the Rule class to achieve the desired result without having to make a custom validation rule?

https://laravel.com/docs/5.4/validation#rule-unique

It's how I was able to pass multiple fields to the 'exists' rule.

Here was my scenario: I have an email_verifications table and I want to ensure that a passed machine code and activation code exist on the same row.

Be sure to include use Illuminate\Validation\Rule;

$activationCode = $request->activation_code;                                   

$rules = [                                                                     
    'mc' => [                                                                  
        'required',                                                            
        Rule::exists('email_verifications', 'machineCode')                     
        ->where(function ($query) use ($activationCode) {                      
            $query->where('activationCode', $activationCode);                  
        }),                                                                    
    ],                                                                         
    'activation_code' => 'required|integer|min:5',                             
    'operating_system' => 'required|alpha_num|max:45'                          
];

The first argument in the exists method is the table and the second is the custom column name I'm using for the 'mc' field. I pass the second column I want to check using the 'use' keyword and then use that field in a a where clause.

bobbybouwmann's avatar

@roerjo Yes the Rule class would be a great example for this. However when this question was asked, this functionality didn't existed!

Anyway thanks for your detailed comment! Now we have two working examples for different versions ;)

1 like
Hiren004's avatar

@bobbybouwmann I've tried your code to create custom validation rule for unique_with but after registering it in config/app.php file still not working!!

I got the error like:

BadMethodCallException in Validator.php line 3295: Method [validateUniqueWith] does not exist.

Any idea what's the problem with it?

bobbybouwmann's avatar

@Hiren004 Please create a new thread with your question and show your code example ;) I might be able to help!

Hiren004's avatar

@bobbybouwmann Now no need to open new thread! I've used other scenario for the same! Anyway, thanks for your response! :)

yanchai's avatar

is it possible to validate 2 input fields like 1st input is country code 2nd input field is mobile number but saved in db as combined values like +8112345 where +81 is the country code and 12345 is the mobile number and the combined value should also be unique?

Please or to participate in this conversation.