jagdish-j-ptl's avatar

Create custom required rule in Laravel validation

My goal is to allow user to select which field is required and which is optional from database.

I have created custom required rule which checks if field is marked as true and is empty then it will fails validation.

It is working fine for required fields i.e. fields which are marked as required. But for other fields which are optional I am getting error like 'The field must be string'. Because it is not set as nullable. If I add nullable then my custom rule is being ignored.

How can I achieve my purpose like if field is not required then it should treat it as nullable.

Below is rule:

'name' => [
    new RequiredRule(),
    'string',
],

Below is RequiredRule:


namespace App\Rules;

use Closure;
use App\Models\RuleTable;
use Illuminate\Contracts\Validation\ValidationRule;

final class RequiredRule implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $attribute = RuleTable::query()->where('field', $attribute)->first();
        
        if ($attribute->required && empty($value)) {
            $fail("The {$attribute->display_name} field is required.");
        }
    }
}
0 likes
8 replies
martinbean's avatar

@jagdish-j-ptl Why not just use the existing Rule::requiredIf rule?

Your custom rule is going to essentially result in N+1 problems, because you’re doing a database query for every field. You should just fetch all required fields in one query, and then add validation for the fields that require it:

class SomeFormRequest extends FormRequest
{
    public function rules(): array
    {
        return [];
    }

    public function withValidator(Validator $validator): void
    {
        // Get all fields that are required
        $attributes = RuleTable::query()->where('required', '=', true)->get();

        // Convert each attribute to associative array of field name => rules
        $rules = $attributes->mapWithKey(function ($attribute) {
            return [
                $attribute->field => ['required'],
            ];
        })->toArray();

        // Now add the rules to the validaor instance
        $validator->addRules($rules);
    }
}
jagdish-j-ptl's avatar

@martinbean

What about the other validations that I need it with field.

Below is my rules array in FormRequest

$rules = [
	'name' => [ new RequiredRule(), 'string', 'max:255'],
	'email' => [ new RequiredRule(), 'email', 'unique:users'],
	'phone' => [new RequiredRule(), 'numeric', 'max:10'],
];

If I correctly understood, your code will create rules for required fields but what about the extra rules? Can I add in rules() method or I should merge rules arrray before $validator->addRules($rules); line?

Tray2's avatar

@jagdish-j-ptl I have a rule that I think does something similar to what you need.

class RequiredIfNotStandalone implements Rule
{
    protected string $series;

    protected bool $isArray = false;

    public function __construct($series)
    {
        if (is_array($series)) {
            $this->isArray = true;
        } elseif ($series === null) {
            $series = '';
        } else {
            $this->series = $series;
        }
    }

    public function passes($attribute, $value): bool
    {
        if ($this->isArray) {
            return false;
        }
        if ($this->series === '') {
            return false;
        }

        if ($value === null) {
            return $this->series === 'Standalone';
        }

        return true;
    }

    public function message(): string
    {
        return 'The part is required when book belongs to a series.';
    }
}

Then I use it in my form request

'part' => [
                new RequiredIfNotStandalone($this->series_name),
                new NumericIfNotStandalone($this->series_name),
            ],

You can find the source here https://github.com/Tray2/mediabase/tree/main

martinbean's avatar

@jagdish-j-ptl Again, you can use the Rule::requiredIf rule.

$requiredFields = RuleTable::query()
    ->where('required', '=', true)
    ->pluck('field')
    ->toArray();

$rules = [
	'name' => [
        Rule::requiredIf(in_array('name', $requiredFields)),
        'string',
        'max:255',
    ],
	'email' => [
        Rule::requiredIf(in_array('email', $requiredFields)),
        'email',
        'unique:users',
    ],
	'phone' => [
        Rule::requiredIf(in_array('phone', $requiredFields)),
        'numeric',
        'max:10',
    ],
];

As an aside, I wouldn’t use numeric for your phone field because people don’t just always type numbers. They’ll type spaces and other formatting characters such as (555) 123-4566 or +4407123456789. Instead, take the user-submitted value and then do a look-up using something like Twilio to verify it and get the E.164 value of that phone number.

jagdish-j-ptl's avatar

@martinbean This was just an example numeric validation on phone. I am not using it. I've tried your solution already but this is also throwing same error The name field must be a string if non-required field is present in request with empty string. But I got alternative solution from this like adding ternary operator to add required or nullable.

$requiredFields = RuleTable::query()
    ->where('required', '=', true)
    ->pluck('field')
    ->toArray();

$rules = [
	'name' => [
        in_array('name', $requiredFields)  ? 'required' : 'nullable',
        'string',
        'max:255',
    ],
	'email' => [
        in_array('email', $requiredFields) ? 'required' : 'nullable',
        'email',
        'unique:users',
    ],
	'phone' => [
        in_array('phone', $requiredFields) ? 'required' : 'nullable',
        'numeric',
        'max:10',
    ],
];

I am using PHP 8.1 might be issue from there due to strict type check.

jagdish-j-ptl's avatar

@Tray2 Thanks for your reply. I am using Laravel 10. Rule class has been depreciated in that and new pass method is not allowed.

I've posted code of RequiredRule file in question. can you update your answer as per Laravel 10 version?

martinbean's avatar
Level 80

@jagdish-j-ptl Just put the nullable rule first. The Rule::requiredIf will still apply if the field is indeed required.

$requiredFields = RuleTable::query()
    ->where('required', '=', true)
    ->pluck('field')
    ->toArray();

$rules = [
	'name' => [
        'nullable',
        Rule::requiredIf(in_array('name', $requiredFields)),
        'string',
        'max:255',
    ],
];

Please or to participate in this conversation.