Complex Conditional Validation with after() Hook

๐Ÿ“– 3 minutes read

Sometimes validation logic for one field depends on another field’s value. For example, imagine a task scheduling form where:

  • If is_scheduled is true, the scheduled_date must be in the future
  • If is_scheduled is false, the scheduled_date (if provided) must be now or in the past

This type of cross-field validation gets messy with nested validation rules:

public function rules(): array
{
    return [
        'is_scheduled' => 'required|boolean',
        'scheduled_date' => [
            'nullable',
            'date',
            function ($attribute, $value, $fail) {
                // Problem: Can't reliably access 'is_scheduled' here
                // It might not be validated yet or could be missing
                $isScheduled = $this->input('is_scheduled');
                // Fragile logic follows...
            },
        ],
    ];
}

Use the after() Validation Hook

Laravel’s after() hook runs after basic validation passes. This means you’re guaranteed that all fields have been validated and are available as clean, typed data:

use Illuminate\Validation\Validator;
use Carbon\Carbon;

public function rules(): array
{
    return [
        'is_scheduled' => 'required|boolean',
        'scheduled_date' => 'nullable|date',
    ];
}

protected function after(): array
{
    return [
        function (Validator $validator) {
            $data = $validator->validated();
            $date = $data['scheduled_date'] ?? null;
            $isScheduled = $data['is_scheduled'];

            if ($date) {
                $parsedDate = Carbon::parse($date);
                
                if ($isScheduled && $parsedDate->isPast()) {
                    $validator->errors()->add(
                        'scheduled_date',
                        'Scheduled date must be in the future.'
                    );
                } elseif (!$isScheduled && $parsedDate->isFuture()) {
                    $validator->errors()->add(
                        'scheduled_date',
                        'Date must be now or in the past for completed tasks.'
                    );
                }
            }
        },
    ];
}

Why This Works Better

  1. Guaranteed valid data: By the time after() runs, you know is_scheduled is a boolean and scheduled_date (if present) is a valid date.
  2. Cleaner logic: No more nested closures in the rules array.
  3. Better testing: Cross-field logic is isolated and easier to test.

Bonus: Use Carbon’s Date Helpers

Instead of manual comparisons with strtotime() or now(), use Carbon’s semantic methods:

$date->isFuture()      // true if date is after now
$date->isPast()        // true if date is before now  
$date->isToday()       // true if date is today
$date->isTomorrow()    // true if date is tomorrow

Your validation logic becomes self-documenting.

Real-World Example: Unique Constraints with Nullable Fields

Another common use case: ensuring uniqueness across multiple fields where some are nullable:

protected function after(): array
{
    return [
        function (Validator $validator) {
            $data = $validator->validated();
            
            // Check if this combo already exists
            $exists = Task::where('user_id', $this->user()->id)
                ->where('project_id', $data['project_id'])
                ->where('is_scheduled', $data['is_scheduled'])
                ->where(function ($query) use ($data) {
                    if ($data['scheduled_date']) {
                        $query->whereDate('scheduled_date', $data['scheduled_date']);
                    } else {
                        $query->whereNull('scheduled_date');
                    }
                })
                ->exists();
                
            if ($exists) {
                $validator->errors()->add(
                    'scheduled_date',
                    'This task already exists with the same scheduling settings.'
                );
            }
        },
    ];
}

Database unique constraints skip NULL values, so this application-level check ensures true uniqueness.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *