Search code examples
phplaravellaravel-5laravel-validationlaravel-request

Laravel validation 'unique' not working as expected


I'm developing a Help Centre for a site and I've asked the question over at the Laracast Forums to which I haven't had a solution from yet unfortunately, here us the routes/url paths for it:

routes.php

Route::resource('admin/help-centre/category', 'AdminHelpCentreCategoryController');
Route::resource('admin/help-centre/category/{category}/section', 'AdminHelpCentreSectionController');
Route::resource('admin/help-centre/category/{category}/section/{section}/article', 'AdminHelpCentreArticleController');

Category and Section validation are working as expected, but for some reason even though it's the same logic, as far as I can tell, the Article validation doesn't seem to work as expected. Here are my validation rules (I've removed the excess and just left the title field):

HelpCentreCategoryRequest.php

/* Insert ------------------------------------- */
    $rules = [
        'title' => 'required|min:3|unique:help_centre_categories',
    ];

/* Update ------------------------------------- */
    if ($method == 'PATCH') {

        $rules = [
            'title' => 'required|min:3|unique:help_centre_categories,title,'.$this->category->id,
        ];

    }

HelpCentreSectionRequest.php

/* Insert ------------------------------------- */
    $rules = [
        'title' => 'required|min:3|unique:help_centre_sections,title,NULL,id,category_id,'.$this->category->id,
    ];


/* Update ------------------------------------- */
    if ($method == 'PATCH') {

        $rules = [
            'title' => 'required|min:3|unique:help_centre_sections,title,'.$this->section->id.',id,category_id,'.$this->category->id,
        ];

    }

I'll explain just to clarify... for Categories, on INSERT it checks to see if that title already exists, if it does, it doesn't validate. UPDATE does exactly the same except it ignores itself in the check so it can still validate if the title hasn't been changed. So I entered 2 Categories, one called Category 1 and the other Category 2.

Same goes for the Section request, except it only checks for Sections which are in the specified Category you're creating it in. I've tested this out and it works. I can insert foo and bar into Category 1 and foo and bar into Category 2, but if I try to add another foo into either Category 1 or Category 2, it doesn't validate cause it's a duplicate, which is correct. But if I try to update foo in Category 1 to bar it won't allow it, and also if I try to update foo without making any changes, it ignores itself (row) allowing it to pass and not throw a validation error. So everything works as it should for this.

Now for my Article validation, I've used the exact same process/query validation as the Section validation, except changing table/column/variables to suit, like this:

HelpCentreArticleRequest.php

/* Insert ------------------------------------- */
    $rules = [
        'title' => 'required|min:3|unique:help_centre_articles,title,NULL,id,section_id,'.$this->section->id,
    ];


/* Update ------------------------------------- */
    if ($method == 'PATCH') {

        $rules = [
            'title' => 'required|min:3|unique:help_centre_articles,title,'.$this->article->id.',id,section_id,'.$this->section->id,
        ];

    }

This should do exactly the same as HelpCentreSectionRequest.php, so allow only one copy of an Article title in each Section of a Category, while allowing an Article of the same title but in a different Section of a Category. That part works fine, however if I insert foo and bar in Category 1 / Section 1, and then update foo to bar it allows the change and to me, it shouldn't since bar already exists in Category 1 / Section 1.

I'm obviously wrong though or else it would work as I expected it too ha. But I just can't see what the problem is?

Here is the database schema just to save any hassle and can confirm table/column names match (again with foreign keys and extra columns removed):

Schema::create('help_centre_categories', function(Blueprint $table)
{
    $table->increments('id')->unsigned();
    $table->string('title');
});

Schema::create('help_centre_sections', function(Blueprint $table)
{
    $table->increments('id')->unsigned();
    $table->integer('category_id')->unsigned();
    $table->string('title');
});

Schema::create('help_centre_articles', function(Blueprint $table)
{
    $table->increments('id')->unsigned();
    $table->integer('category_id')->unsigned();
    $table->integer('section_id')->unsigned();
    $table->string('title');
});

I've also checked the if statement in the Article request is definitely firing which was mentioned in the Laracast Forum link at the top. You can also find the Valiation docs here, but for easy reading, this is the main section I am using:

Adding Additional Where Clauses

You may also specify more conditions that will be added as "where" clauses to the query:

'email' => 'unique:users,email_address,NULL,id,account_id,1'

In the rule above, only rows with an account_id of 1 would be included in the unique check.

Solution

  • Well, first of all your code is hard to analyze, If I were you, I would use array syntax for that:

       $rules = [
            'title' => ['required'. 'min:3'],
       ];
       $uniqueRule = 'unique:help_centre_categories';
    
       /* Update ------------------------------------- */
        if ($method == 'PATCH') {
           $uniqueRule.='title,'.$this->category->id
        }
        $rules['title'][] = $uniqueRule;
    

    The second thing is the $method variable. I don't know where it comes from, but assuming it holds the correct value, you should probably change your condition this way:

    if (strtoupper($method) == 'PATCH' || strtoupper($method) == 'PUT') {
     ...
    }
    

    Last thing, if I try to use one Request class both for store and update, I usually do it this way:

    $segments = $this->segments();
    $id = intval(end($segments));
    if ($id != 0) {
     // now you know it's update
    }
    

    If your method does not work, you could try this above.

    EDIT

    Inside your Request object you can use getMethod() method, for example:

    public function rules() 
    {
       $method = $this->getMethod();
       if ($method == 'PUT' || $method == 'PATCH') {
            // now you do what you want for update
       }
    
       // rest of code goes here
    }
    

    For update you should verify if $method is PUT or PATCH