Conditional validation rules in Laravel

Laravel lets you conditionally add validation rules using the Validator::sometimes instance method. That works well, but there are times when you don’t have access to the validator instance, and don’t want to manually construct one.

Let’s take a look at how you can conditionally apply validation rules in those situations.

The problem

Imagine we have a store method, which creates a new customer order.

// use Illuminate\Http\Request;

public function store(Request $request)
{
    // Validation goes here...

    $order = Order::create($validated);
}

Before saving the order, we need to validate the delivery details. These are the rules we need to enforce:

  • If the delivery_provider is “fedex”, the delivery_service must be “next_day” or “two_day”.
  • If the delivery_provider is “ups”, the delivery_service must be “express” or “standard”.
  • If the delivery_provider is “usps”, the delivery_service must be “two_day” or “someday”.

Define a map of validation rules

You may have noticed that the above rules constitute a map, where the key is the delivery provider, and the value is an array of services offered by that provider:

$rules = [
    'fedex' => ['next_day', 'two_day'],
    'ups'   => ['express', 'standard'],
    'usps'  => ['two_day', 'someday'],
];

From here, it’s a short step to convert the values in our map to Laravel-friendly validation rules:

$rules = [
    'fedex' => ['required', 'in:next_day,two_day'],
    'ups'   => ['required', 'in:express,standard'],
    'usps'  => ['required', 'in:two_day,someday'],
];

Determine which rules to apply

The next step is figuring out which set of rules to apply. That’s easy enough; we just need to retrieve the chosen delivery provider:

$provider = $request->get('delivery_provider');

$serviceRules = array_key_exists($provider, $rules)
    ? $rules[$provider]
    : [];

A working solution

At this point, we have everything we need to validate the delivery service. Here’s our updated store method:

public function store(Request $request)
{
    $rules = [
        'fedex' => ['required', 'in:next_day,two_day'],
        'ups'   => ['required', 'in:express,standard'],
        'usps'  => ['required', 'in:two_day,someday'],
    ];

    $provider = $request->get('delivery_provider');

    $serviceRules = array_key_exists($provider, $rules)
        ? $rules[$provider]
        : [];

    $validated = $request->validate([
        'delivery_service' => $serviceRules,
    ]);

    $order = Order::create($validated);
}

A neater solution

That all works, but it can be a little verbose. To keep things neat, I created Apposite. Here’s our store method, refactored to use Apposite’s ApplyMap validation rule:

// use Monooso\Apposite\ApplyMap;

public function store(Request $request)
{
    $serviceRules = new ApplyMap(
        $request->get('delivery_provider'),
        [
            'fedex' => ['required', 'in:next_day,two_day'],
            'ups'   => ['required', 'in:express,standard'],
            'usps'  => ['required', 'in:two_day,someday'],
        ]
    );

    $validated = $request->validate([
        'delivery_service' => [$serviceRules],
    ]);

    $order = Order::create($validated);
}

Apposite also includes ApplyWhen and ApplyUnless, which apply validation rules according to a boolean conditional, and a few other useful enhancements.

Check out the GitHub repository for full installation and usage instructions.

Sign up for my newsletter

A monthly round-up of blog posts, projects, and internet oddments.