When your app stores date ranges (like a room reservation), you may need to ensure that no two records overlap.
There is nothing built in to Laravel that does this, but we can easily write a custom rule that checks the database for conflicts with existing records.
This works great when creating new date ranges, but you'll run into an issue during updates. Depending on the new values, the rule could see the record you’re editing as a collision with itself, so the validation fails when it shouldn't.
To solve this, we need to tell the rule to ignore the record being updated. We can add one method to our custom rule class:
public function ignore(?Reservation $reservation = null): self
{
$this->ignore = $reservation;
return $this;
}
And then we add a little bit of logic to our rule to honor that ignored record:
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$query = Reservation::where('starts_at', '<=', $value)
->where('ends_at', '>=', $value);
if ($this->ignore) {
$query->where($this->ignore->getKeyName(), '!=', $this->ignore->getKey());
}
if ($query->exists()) {
$fail('validation.reservation_date_no_overlap')->translate();
}
}
Not only does this work, and it's simple to implement, but I love that it mirrors how the built-in unique
rule works.
In our rule, we chose to only ignore a model, whereas the built-in unique
rule can also be passed the id for a model.
It would be trivial to extend this to support that, but we didn't need it for our use case.
Here to help,
Joel
P.S. Aaron and I have thought a lot about validation rules. We captured our hard-earned knowledge in the Mastering Laravel Validation Rules book.