Table of Contents
๐ 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_scheduledistrue, thescheduled_datemust be in the future - If
is_scheduledisfalse, thescheduled_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
- Guaranteed valid data: By the time
after()runs, you knowis_scheduledis a boolean andscheduled_date(if present) is a valid date. - Cleaner logic: No more nested closures in the rules array.
- 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.
Leave a Reply