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

SteveBelanger's avatar

Custom validation involving query

I want to add a validation rule slightly more complex than the ones out of the box.
If a user wants to pick a task, he should be prevented from doing so if that causes a schedule conflict. I am new to the mvc pattern and the use of request classes for validation. I have put this query directly in my controller :

$ConflictsWithSchedule = DB::table('tasks')
            ->Where(function ($query) use ($start, $end){
                $query->where('prof', '=', Auth::user()->name) 
                      ->whereBetween('start', [$start, $end])
                      ->orwhere('end', '>', $start)
                        ->where('end', '<', $end)
                      ->orwhere('start', '<=', $start)
                        ->where('start', '>=', $end);
            })->lists('task');
        if(count($ConflictsWithSchedule)) // if anything in user schedule conflicts with what he wants to add to it
        {
            // handle this validation failure here
        }

I am confused as to where I should put this query or program the logic to "clean my controller" and have this custom validation performed in a Request class like so :

public function( takeTaskRequest $request){
    // validation is done in takeTaskRequest and controller looks "cleaner"
}

thanks in advance for any help

0 likes
5 replies
SteveBelanger's avatar

@Prez I started looking in Laravel's doc here Is that what you are refering to ?

I am confused about the proposed code in the documentation :


class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('foo', function($attribute, $value, $parameters, $validator) {
            return $value == 'foo';
        });
    }

Is foo a placeholder for the name of my rule ? -- let's call my custom rule 'noConclicts' Then, I place the error message in my language file

"noConflicts => "Some task already scheduled are in conflicts with the task you attempt to add."

... and then it is going to work with nothing else to do ?

I would put this custom rule in my takeTaskRequest file :

'start' => 'required|noConflicts'
'end'  // ho no!!  start and end have to be validated as a pair...

So the generic question " is it the way to add a custom rule ?" is one question but another one arise in that specific case : Is it possible to apply validation logic concerning a combination of fields, and not one field at a time ?

andremellow's avatar

@SteveBelanger

This is my complete soluction:

create the custom Validation Class and do everything you need to do!

<?php 

namespace App\Vilidations;

use Illuminate\Support\Facades\DB;

class UniqueByTenantValidator extends \Illuminate\Validation\Validator
{
        function validateUniqueByTenant( $attribute, $value, $parameters, $validator ) 
        {
            return UniqueByTenantValidator::uniqueByTenant( $attribute, $value, $parameters ) ; 
        }

        static function uniqueByTenant( $attribute, $value, $parameters ) {
                
            $table = $parameters[0];
            $field = $parameters[1];
            $tenant_id = $parameters[2];
            $excluded_id = $parameters[3];
            
            // query the table with all the conditions
            $result = DB::table( $table )->select( \DB::raw( 1 ) )
            ->where($field , $value)
            ->where(function($query) use( $tenant_id ){
                $query->where( 'tenant_id' , $tenant_id )
                      ->orWhereNull('tenant_id');

            });

            if( $excluded_id != 'NULL' )
            {
                $result->where( 'id' , '!=',  $excluded_id );
            }

            $result = $result->get();

            return empty( $result ); // edited here
        }
}

Now you can call in your model like this

....
class County extends Model
{
    protected $fillable = ['name' , 'references'];

     public static function boot(){
        parent::boot();
        
        static::saving( function($county){

            $attribuites =  [
                $county->getTable() ,
                'name' ,
                $county->tenant_id ,
                $county->id == null ? 'NULL' : $county->id
            ];


            if( ! UniqueByTenantValidator::uniqueByTenant( 'name' , $county->name, $attribuites  ) ){
                    throw new UniqueByTenantException( trans('unique_by_tenant' , ['attribuite' => 'name']) , 1);
            } 
        });

    }
}

Or in you Request Validator

public function rules()
    {

        $user = Auth::user();
         switch($this->method())
        {
            case 'GET':
            case 'DELETE':
            {
                return [];
            }
            case 'POST':
            {
                return [
                    'county.name'          => 'required|unique_by_tenant:counties,name,'.$user->getCurrentTenantId().',NULL'
                ];
            }
            case 'PUT':
            case 'PATCH':
            {
                return [
                    'county.name'          => 'required|unique_by_tenant:counties,name,'.$user->getCurrentTenantId().','.Request::route('id')
                ];
            }
            default:break;
        }

       

    }

Don't forget to register it on ServiceProvider

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
use App\Vilidations\UniqueByTenantValidator;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        // ...
        Validator::resolver(function($translator, $data, $rules, $messages)
        {
          return new UniqueByTenantValidator($translator, $data, $rules, $messages);
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
2 likes
SteveBelanger's avatar

Thank you guys !! You are pointing me in the right directions. I will have to tinker a little. I guess andremellow is using 5.0 because Validator::resolver does not seem to exist in 5.1, so I will myself::resolve this slight difference. Prez idea of using helpers is a tempting shortcut that may very well work. I am advancing slowly because I am doing this on spare time. ... Ouch! my brain hurts figuring out this maze of classes, facades, parameters...

andremellow's avatar

@SteveBelanger , I'm at Laravel 5.2:

"laravel/framework": "5.2.*",

Have you imported the Facade ? use Illuminate\Support\Facades\Validator;

UniqueByTenantValidator is a simple class. You can place it wherever you want. I placed at app\Vilidations

You will use here:

public function boot()
    {
        // ...
        Validator::resolver(function($translator, $data, $rules, $messages)
        {
          return new UniqueByTenantValidator($translator, $data, $rules, $messages);
        });
    } 

You will need to import the class (use App\Vilidations\UniqueByTenantValidator;) beyond the facade

SteveBelanger's avatar

@andremellow

With your's and others help, I am almost there. my request :


namespace App\Http\Requests;

use App\Http\Requests\Request;
use Illuminate\Support\Facades\Auth;

class askSubstitutionRequest extends Request
{
//...
public function rules()
    {
        return [
            'datetimeDebut' => 'required|date_format:d#m#Y H#i|after:now|aconflict:datetimeDebut,datetimeFin,'.Auth::user()->name.'',
            'datetimeFin'  => 'required|date_format:d#m#Y H#i|after:datetimeDebut'
            //
        ];
    }
}

my validator :

namespace App\Validators;
use DB;

class scheduleConflictValidator
{    
public function validateScheduleConflicts($attribute,$value, $parameters, $validator)
    {

        $d = $parameters[0];
        $f = $parameters[1];
        $username = $parameters[2];
        /*
        $d = '24/05/2016 16:52';
        $f = '25/05/2016 16:53';
        $username = 'user1';
        */

        // Laravel's datetime format seems to be d#m#Y H#i , but MySql needs Y-m-d H:i:s
        $debutt = date_create_from_format('d#m#Y H#i', $d);  // d#m#Y H#i format returned by js datetimepicker
        $debut = (string)date_format($debutt, 'Y-m-d H:i:s');   // format suitable for mysql database
        $finn = date_create_from_format('d#m#Y H#i', $f);
        $fin = (string)date_format($finn, 'Y-m-d H:i:s');

        $conflicts = somequery, irrelevant here
            })->get();

        $otherConflicts = another query
            })->get();
        //dd($parameters);
        return empty($conflicts or $otherConflicts); // true when no schedule conflicts
       
}

If I hardcode my parameters, I can see it works. If I want to use the parameters passed from the request to the validator, I get this error : date_format() expects parameter 1 to be DateTimeInterface, boolean given when i dd() parameters, I get :

array:3 [▼ 0 => "datetimeDebut" 1 => "datetimeFin" 2 => "user1" ]

I am using the post method I am with Laravel 5.1

I am confused as to how to debug this :
Why does it say that it is a boolean in the error message but seems to give me a string of the name of the attribute ? How will I get it to pass a the date string which is in the form being submitted ?

Please or to participate in this conversation.