Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

randm's avatar

How to check for unique column combanation from the validation rule?

Hi, I am using Laravel 5.1.

I created a a request class that handles a forum validation. Additionally, I have a unique combined index on the table like this

$table->unique( array('company_code', 'client_id') );

How can I add rule for the company_code field, to ensure that the field is

  1. required
  2. string length = 18 characters
  3. unique combination on (company_code, client_id)

Here is how my current request class looks like

<?php

namespace App\Http\Requests\accounts;

use App\Http\Requests\Request;

class create extends Request
{
    /**
     * 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 [
            'account_name'  => 'required|min:5|max:100',
            'legal_name'    => 'max:100',
            'company_code'  => 'required|unique:accounts,company_code, client_id',
            'client_id'     => 'required|min:1',

        ];
    }
}

Thank you for your help

0 likes
33 replies
bimalshah72's avatar

@malhayek

Regarding,

required string length = 18 characters

'company_code'  => 'required|size:18'

Regarding composite unique key

I think It is not possible out of the box in Laravel. You have to write a custom validator or use a package for that.

One package is: - https://github.com/felixkiss/uniquewith-validator

With this package, your rules could look like the following:

'company_code'  => 'required|size:18|unique_with:client_id' //here you can add any number of composite unique, comma separated 
3 likes
FoxRocks's avatar

@bimalshah72 Thank you sir, that package you mentioned was exactly what I needed and I'm not sure I would have found it without you. Cheers!

rodrigo.pedra's avatar

EDIT corrected the return from the validation... sorry for that

@bimalshah72 is correct, Laravel does not come with unique validation for composite unique keys out of the box, but it is easy to create a custom validator for that:

    public function rules()
    {
        // extends Validator only for this request
        \Validator::extend( 'composite_unique', function ( $attribute, $value, $parameters, $validator ) {
                
                // remove first parameter and assume it is the table name
                $table = array_shift( $parameters ); 

                // start building the conditions
                $fields = [ $attribute => $value ]; // current field, company_code in your case

                // iterates over the other parameters and build the conditions for all the required fields
                while ( $field = array_shift( $parameters ) ) {
                    $fields[ $field ] = $this->get( $field );
                }

                // query the table with all the conditions
                $result = \DB::table( $table )->select( \DB::raw( 1 ) )->where( $fields )->first();

                return empty( $result ); // edited here
            }, 'your custom composite unique key validation message' );

        return [
            'account_name' => 'required|min:5|max:100',
            'legal_name'   => 'max:100',
            // note that here the custom validator is used
            'company_code' => 'required|size:18|composite_unique:accounts,client_id', 
            'client_id'    => 'required|min:1',
        ];
    }
1 like
randm's avatar

Thank you so much for your help @bimalshah72 . @rodrigo.pedra Thank you for this snip it. How can I place this code in my environment where I can reuse it? I have many table with a unique combination indexes and I hate to re-write my code over and over. The ultimate goal is only need to add the keyword "composite_unique" in the rule and Laravel then knows what to do with it.

Thank you for your help

martinbean's avatar

@malhayek Put it in a service provider class:

class ValidationServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app['validator']->extend('composite_unique', function ($attribute, $value, $parameters, $validator) {
            // Custom validation logic
        });
    }
}
2 likes
rodrigo.pedra's avatar

@martinbean solution will do it, another option is to create a custom Validator class:

  1. Create a class that extends Illuminate\Validation\Validator, you can place it anywhere inside your app folder:
<?php
// ./app/Validator/MyValidator.php

namespace App\Validator;

use Illuminate\Validation\Validator;

class MyValidator extends Validator
{
    // Laravel uses this convention to look for validation rules, this function will be triggered 
    // for composite_unique
    public function validateCompositeUnique( $attribute, $value, $parameters )
    {
        // by extending you can use protected methods like this one
        $this->requireParameterCount( 2, $parameters, 'composite_unique' );

        // same logic from my last response
        // ...
    }

    // you can add other validations to this class, the next one will validate a another_rule validation
    public function validateAnotherRule( $attribute, $value, $parameters )
    {
        // custom logic here
    }
}
  1. Create a ServiceProvider to add it to the IoC container or add it to a current ServiceProvider's boot()method, here for the sake of simplicity I will add to AppServiceProvider which already ships with Laravel:
<?php
// ./app/Providers/AppServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

use App\Validator\MyValidator;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        \Validator::resolver(
            function ( $translator, $data, $rules, $messages, $customAttributes ) {
                return new MyValidator( $translator, $data, $rules, $messages, $customAttributes );
            }
        );
    }

    public function register()
    {
        // ...
    }
}
  1. For the error message, add an entry to your validation.php file in your resources folder:
<?php
// ./resources/lang/en/validation.php

return [

    'composite_unique' => 'My fancy custom validation message for :attribute',
    
    // other rules and attributes ...

];

Now you should have the rule available everywhere in your application

2 likes
randm's avatar

Thank you @martinbean for the code

Please bare with me as I am not to Laravel and to frameworks in general. I am not getting an error "Method [validateCompositeUnique] does not exist."

Here is what I have done

  1. From the console I run "php artisan make:provider ValidationServiceProvider"
  2. opened the ValidationServiceProvider.php file located on "app\Providers"

Then created the provider with the following code

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ValidationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //

        $this->app['validator']->extend('composite_unique', function ($attribute, $value, $parameters, $validator) {
            // Custom validation logic

            // remove first parameter and assume it is the table name
            $table = array_shift( $parameters ); 

            // start building the conditions
            $fields = [ $attribute => $value ]; // current field, company_code in your case

            // iterates over the other parameters and build the conditions for all the required fields
            while ( $field = array_shift( $parameters ) ) {
                $fields[ $field ] = $this->get( $field );
            }

            // query the table with all the conditions
            $result = \DB::table( $table )->select( \DB::raw( 1 ) )->where( $fields )->first();

            return !empty( $result );
        }, 'This value :attribute already exists!');
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Then in my request file this is what I have done

<?php

namespace App\Http\Requests\accounts;

use App\Http\Requests\Request;

class create extends Request
{
    /**
     * 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 [
            'account_name'  => 'required|min:5|max:100',
            'legal_name'    => 'max:100',
            'company_code'  => 'required|size:18|composite_unique:accounts,client_id',
            'client_id'     => 'required|min:1',

        ];
    }
}

What else am I missing here?

rodrigo.pedra's avatar

You should register any custom ServiceProviders in your ./config/app.php:

<?php

return [
    // ...
    'providers'       => [
        // ... 
        // add your custom ServiceProvider to the end of this list
        'App\Providers\ValidationServiceProvider',
    ],
    // ...
];
randm's avatar

@rodrigo.pedra Thank you again. I added the provider to the config file like so

App\Providers\ValidationServiceProvider::class,

I am not sure if Laravel 4 is a bit different than laravel 5 but I replicated the other lines in the Providers array.

now when I try to use this validation rule I get a new error

BadMethodCallException in ServiceProvider.php line 234: Call to undefined method [get]

    in ServiceProvider.php line 234
    at ServiceProvider->__call('get', array('client_id')) in ValidationServiceProvider.php line 29
    at ValidationServiceProvider->get('client_id') in ValidationServiceProvider.php line 29
    at ValidationServiceProvider->App\Providers\{closure}('company_code', '5556', array('accounts', 'client_id'), object(Validator))
    at call_user_func_array(object(Closure), array('company_code', '5556', array('accounts', 'client_id'), object(Validator))) in Validator.php line 2510
    at Validator->callExtension('composite_unique', array('company_code', '5556', array('accounts', 'client_id'), object(Validator))) in Validator.php line 2597
    at Validator->__call('validateCompositeUnique', array('company_code', '5556', array('accounts', 'client_id'), object(Validator))) in Validator.php line 362
    at Validator->validateCompositeUnique('company_code', '5556', array('accounts', 'client_id'), object(Validator)) in Validator.php line 362
    at Validator->validate('company_code', 'composite_unique:accounts,client_id') in Validator.php line 314
    at Validator->passes() in ValidatesWhenResolvedTrait.php line 24
    at FormRequest->validate() in ValidationServiceProvider.php line 32
rodrigo.pedra's avatar

Oops, my implementation is coupled with the Request class... :(

But you can get away with the Request façade

change this:

            // iterates over the other parameters and build the conditions for all the required fields
            while ( $field = array_shift( $parameters ) ) {
                $fields[ $field ] = $this->get( $field ); // expects $this to be an instance (or sub-class) of Request
            }

to this:

            // iterates over the other parameters and build the conditions for all the required fields
            while ( $field = array_shift( $parameters ) ) {
                $fields[ $field ] = \Request::get( $field ); // using Request facade
            }
rodrigo.pedra's avatar

By the way, I am using Laravel 5.1, the ::class syntax is new to PHP 5.5, which is required by Laravel 5.1, I used the class name between quotes as it also works and many developers working with old code bases are used to it.

For new projects I prefer the ::class syntax, it makes PHPStorm happy and makes easier to navigate between source files.

1 like
randm's avatar

@rodrigo.pedra thank you for your comment about the ::class syntax and for correcting the code.

Note, I updated my code to reflect your edited post with the validateCompositeUnique logic I am still running into an issue

it seems to be working backward. if there is valid data I get this error "This value of company code already exists." and where there is dup I get this error

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '0' for key 'accounts_external_id_client_id_unique' (SQL: insert into `accounts` (`account_name`, `legal_name`, `company_code`, `modified_on`, `created_on`) values (Test 75, RDI, 345, 2015-07-28 17:05:14, 2015-07-28 17:05:14))

Again I apologize for being suck a newbie with this.

rodrigo.pedra's avatar

Noticed that you edited it after posting, no problem.

Where is the client_id attribute in the insert query?

insert into `accounts` (`account_name`, `legal_name`, `company_code`, `modified_on`, `created_on`) values ('Test 75', 'RDI, 345', '2015-07-28 17:05:14', '2015-07-28 17:05:14')

Did you add the client_id attribute to the $fillable array on your Accounts model?

As it is not inserting this attribute properly, it defaults to zero, so in a second insert the unique constraint will not do it.

If you are populating your model with the constructor, create(...) or fill(...) methods, you should add it to the model's $fillable property

randm's avatar

Great point @rodrigo.pedra

client_id was not in the $fillable array. I just added it but still running into the same issue "It is running with the opposite logic"

QueryException in Connection.php line 636: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '345-1' for key 'accounts_company_code_client_id_unique' (SQL: insert into `accounts` (`account_name`, `legal_name`, `company_code`, `client_id`, `modified_on`, `created_on`) values (Test 75, RDI, 345, 1, 2015-07-28 17:15:37, 2015-07-28 17:15:37))

    in Connection.php line 636
    at Connection->runQueryCallback('insert into `accounts` (`account_name`, `legal_name`, `company_code`, `client_id`, `modified_on`, `created_on`) values (?, ?, ?, ?, ?, ?)', array('Test 75', 'RDI', '345', '1', '2015-07-28 17:15:37', '2015-07-28 17:15:37'), object(Closure)) in 
rodrigo.pedra's avatar

Please add a call to dd(...) after the results from DB inside the validation method and paste the results here

// inside the validation function

// query the table with all the conditions
$result = \DB::table( $table )->select( \DB::raw( 1 ) )->where( $fields )->first(); 

dd($result); // add this line

dd(...) is a function useful for debugging

randm's avatar

Here is what I get from the dd() output {#181 ▼ +"1": 1 }

FYI: i removed the size:18 rule and I am only passing the value 1 for the client_id and the value 456 for the company_code which should give me a dup error

rodrigo.pedra's avatar

Huge mistake of mine...

remove the exclamation mark (!) from the return condition:

return empty( $result ); // remove the !

If any record is found, then the record you are about to create is not unique... the truthy condition should be "no records found"

1 like
randm's avatar

Thank you so much!!! I can't say thank you enough. This is very complicated to me "may be because I am new to Laravel" but it works.

if I have 3 or 4 combination column should I just update my validation rule like so?

'company_code'  => 'required|composite_unique:accounts,client_id, col2, col3.....',
rodrigo.pedra's avatar
Level 56

You're welcome. Don't be afraid to be "new to Laravel" it is a wonderful framework with two great advantages: the wonderful videos here at laracasts and the very well written source code. Whenever you have doubts on how something works, dig into the source code, it can be scary at first, but it is so well written that you'll get used to it very fast.

Regarding using multiple attributes, It should work, as we are iterating through all the sent parameters and adding them in our where condition.

2 likes
randm's avatar

Thank you so much again :)

1 like
randm's avatar

@rodrigo.pedra sorry to bug you again. but when I apply the same rule on the update request I get a problem. the problem is that it does not ignore the record it is editing.

for example UPDATE accounts SET account_name = 'Test' WHERE account_id = 5;

The composite_unique rule will do a query that looks like this

SELECT 1 FROM accounts
WHERE company_code = '567' AND client_id = '1' 
LIMIT 1;

such a query works fine when trying to create a new record but not when updating a record. for updating a record the query should be like this

SELECT 1 FROM accounts
WHERE company_code = '567' AND client_id = '1' AND account_id <> 5 
LIMIT 1;

How can I tweak the rule to ignore the primaryKey on update?

rodrigo.pedra's avatar

I made some changes, read the comments before the function, there were some changes in usage

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class ValidationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        /**
         * composite_unique validation rule
         *
         * Usage:
         *
         * When creating:
         *
         *      'column1' => 'composite_unique:table,column1,column2'
         *      'column1' => 'composite_unique:table,column1,column2,column3'
         *
         * When Updating, use a number or an expression with an equal sign as the
         * last parameter
         *
         *      'column1' => 'composite_unique:table,column1,column2,10'
         *      'column1' => 'composite_unique:table,column1,column2,primaryField = 10'
         *
         * IMPORTANT: note that the current column should be added again in the rule, this 
         * permits the attribute name being validated to be different than the database column
         */
        $this->app[ 'validator' ]->extend( 'composite_unique',
            function ( $attribute, $value, $parameters, $validator ) {
                // Custom validation logic

                // data being validated
                $data = $validator->getData();

                // remove whitespaces
                $parameters = array_map( 'trim', $parameters );

                // remove first parameter and assume it is the table name
                $table = array_shift( $parameters );

                // remove last parameter to check for except condition
                $lastParameter = array_pop( $parameters );

                // start building the query
                $query = \DB::table( $table )->select( \DB::raw( 1 ) );

                // add the field being validated as a condition
                // IMPORTANT: skipping it for improved consistency, see
                // note in the function's comment
                // $query->where( $attribute, $value );

                // iterates over the parameters and add as where clauses
                while ($field = array_shift( $parameters )) {
                    $query->where( $field, array_get( $data, $field ) );
                }

                // check $lastParameter for except condition. Uses a regular
                // expression to check if $lastParameter contains only numbers
                // or an equal sign
                if (preg_match( '/^(?:\d+|.+?=.+)$/', $lastParameter )) {
                    // is except condition

                    if (preg_match( '/^\d+$/', $lastParameter )) {
                        // only numbers, assume primary key is called 'id' rewrite $lastParameter
                        $lastParameter = sprintf( '%s.id = %s', $table, $lastParameter );
                    }

                    // negate condition
                    $exceptField = sprintf( '(NOT %s)', $lastParameter );

                    $query->whereRaw( $exceptField );
                } else {
                    // is not except condition, add as a normal where
                    $query->where( $lastParameter, array_get( $data, $lastParameter ) );
                }

                // get the result from DB
                $result = $query->first();

                return empty( $result ); // true if no result was found
            } );
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
1 like
randm's avatar

Hello Again, thank you for this update. It is not working for the update I get an error "This value of company code already exists."

Additionally, how would i pass the actual value of the id to the rule? and in my case the primaryKey is account_id not id and it could be different from one Model to another.

Here is my current Rule

    public function rules()
    {
        return [
            'account_name'  => 'required|min:5|max:100',
            'legal_name'    => 'max:100',
            'company_code'  => 'required|composite_unique:accounts,client_id,account_id=1', 
            'client_id'     => 'required|min:1',

        ];
    }

Here is my CustomValidationRule.php

<?php

namespace App\Classes\Validator;

use Illuminate\Validation\Validator;
use DB;
use Request;
class CustomValidationRules extends Validator
{

    /**
     * composite_unique validation rule
     *
     * Usage:
     *
     * When creating:
     *
     *      'column1' => 'composite_unique:table,column1,column2'
     *      'column1' => 'composite_unique:table,column1,column2,column3'
     *
     * When Updating, use a number or an expression with an equal sign as the
     * last parameter
     *
     *      'column1' => 'composite_unique:table,column1,column2,10'
     *      'column1' => 'composite_unique:table,column1,column2,primaryField = 10'
     *
     * IMPORTANT: note that the current column should be added again in the rule, this 
     * permits the attribute name being validated to be different than the database column.
     *
     * Additionally:
     * Laravel uses this convention to look for validation rules, this function will be triggered 
     * for composite_unique
    */


    public function validateCompositeUnique( $attribute, $value, $parameters, $validator )
    {

        // data being validated
        $data = $validator->getData();

        // remove whitespaces
        $parameters = array_map( 'trim', $parameters );

        // remove first parameter and assume it is the table name
        $table = array_shift( $parameters );

        // remove last parameter to check for except condition
        $lastParameter = array_pop( $parameters );

        // start building the query
        $query = DB::table( $table )->select( DB::raw( 1 ) );

        // add the field being validated as a condition
        // IMPORTANT: skipping it for improved consistency, see
        // note in the function's comment
        // $query->where( $attribute, $value );

        // iterates over the parameters and add as where clauses
        while ($field = array_shift( $parameters )) {
            $query->where( $field, array_get( $data, $field ) );
        }

        // check $lastParameter for except condition. Uses a regular
        // expression to check if $lastParameter contains only numbers
        // or an equal sign
        if (preg_match( '/^(?:\d+|.+?=.+)$/', $lastParameter )) {
            // is except condition

            if (preg_match( '/^\d+$/', $lastParameter )) {
                // only numbers, assume primary key is called 'id' rewrite $lastParameter
                $lastParameter = sprintf( '%s.account_id = %s', $table, $lastParameter );
            }

            // negate condition
            $exceptField = sprintf( '(NOT %s)', $lastParameter );

            $query->whereRaw( $exceptField );
        } else {
            // is not except condition, add as a normal where
            $query->where( $lastParameter, array_get( $data, $lastParameter ) );
        }

        // get the result from DB
        $result = $query->first();

        return empty( $result ); // true if no result was found

    }
}

Here is the code for my ValidationServiceProvider

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Classes\Validator\CustomValidationRules;
use DB;
use Validator;


class ValidationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //

         Validator::resolver(
            function ( $translator, $data, $rules, $messages, $customAttributes ) {
                return new CustomValidationRules( $translator, $data, $rules, $messages, $customAttributes );
            }
        );
        
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

rodrigo.pedra's avatar
  1. I changed the rule parsing, so now you must provide all the columns you want to validate as a unique composite, including the one that owns the rule, like so:
public function rules()
    {
        return [
            'account_name'  => 'required|min:5|max:100',
            'legal_name'    => 'max:100',

            // added company_code to the rules
            'company_code'  => 'required|composite_unique:accounts,company_code,client_id,account_id=1',  
            'client_id'     => 'required|min:1',

        ];
    }
  1. The account_id is fine, the validation will check for the equal sign and use it as a raw where condition

  2. Regarding dynamic id value you must replace it manually, like so:

public function rules()
    {
        return [
            'account_name'  => 'required|min:5|max:100',
            'legal_name'    => 'max:100',

            // concatenates the id
            'company_code'  => 'required|composite_unique:accounts,company_code,client_id,account_id=' . $this->get('id'),  
            'client_id'     => 'required|min:1',

        ];
    }

For larger applications, consider adding a method to the ./app/Http/Requests/Request.php class to handle this ID replacement.

1 like
randm's avatar

It worked! Thank you so much for all your work help me :)

This is what I change my rule to look like

This is what I have done in the validationRule now 'company_code' => 'required|composite_unique:accounts,company_code,client_id,account_id=' . $this->get(Accounts::primaryKey),

Regarding your comment "For larger applications, consider adding a method to the ./app/Http/Requests/Request.php class to handle this ID Replacement."

If this what you asking me to do?

    public function getPrimaryKeyValue($table){
        $primary = $this->table($table)->primaryKey;

        return $primary .'=' . $this->get()

    }

How could this help me?

gildniy's avatar

How can the message of format :attribute and :other be formulated to give something like this:

Fields pair company_code and client_id already exists in our records

abdallahadel's avatar

@rodrigo.pedra Hi Mr. Rodrigo,

please can give us the solution as custom validation class , because i am confused with many modifications to solution :)

garima90's avatar

Hi, The above custom validation method is not allowing me to update the own record. for example-

first name : abc last name : xyz city : new york

composite unique is applied on first name and last name, now if I m trying to update city column then it is giving me error for duplicate entry for first name.

Please help asap

Thanks in advance

Next

Please or to participate in this conversation.