Table of Contents
📖 2 minutes read
When creating RESTful resources, your UpdateRequest and StoreRequest often share 95% of the same validation logic. Here’s how to keep it DRY without sacrificing clarity.
The Problem
Typical approach duplicates the entire validation:
// StoreClimbRequest
public function rules(): array
{
return [
'data.attributes.trail_id' => ['required', 'uuid', 'exists:trails,id'],
'data.attributes.date' => ['nullable', 'date'],
];
}
public function after(): array
{
return [
function (Validator $validator) {
// Check uniqueness for trail_id + date
$exists = Climb::where('author_id', $authorId)
->where('trail_id', $trailId)
->where(function ($query) use ($date) {
if ($date === null) {
$query->whereNull('date');
} else {
$query->where('date', $date);
}
})
->exists();
if ($exists) {
$validator->errors()->add('...', '...');
}
},
];
}
// UpdateClimbRequest - DUPLICATES EVERYTHING except one ->where()
class UpdateClimbRequest extends FormRequest
{
// Duplicate rules(), duplicate after(), ONLY difference:
// ->where('id', '!=', $this->route('climb')->id)
}
The Solution: Extract the Query
1. Make UpdateRequest extend StoreRequest:
class UpdateClimbRequest extends StoreClimbRequest
{
// Inherits rules() and after() from parent
}
2. Extract the uniqueness query into a protected method in StoreRequest:
class StoreClimbRequest extends FormRequest
{
public function after(): array
{
return [
function (Validator $validator) {
if ($validator->errors()->isNotEmpty()) {
return;
}
$attributes = $this->input('data.attributes');
$authorId = $this->user()->author->id;
$trailId = $attributes['trail_id'];
$date = $attributes['date'] ?? null;
// Use the extracted query method
$exists = $this->uniquenessQuery($authorId, $trailId, $date)->exists();
if ($exists) {
$validator->errors()->add(
'data.attributes.trail_id',
'You have already added this trail' . ($date ? ' for this date' : ' without a date') . '.'
);
}
},
];
}
protected function uniquenessQuery(string $authorId, string $trailId, ?string $date)
{
return Climb::where('author_id', $authorId)
->where('trail_id', $trailId)
->where(function ($query) use ($date) {
if ($date === null) {
$query->whereNull('date');
} else {
$query->where('date', $date);
}
});
}
}
3. Override ONLY the difference in UpdateRequest:
class UpdateClimbRequest extends StoreClimbRequest
{
protected function uniquenessQuery(string $authorId, string $trailId, ?string $date)
{
return parent::uniquenessQuery($authorId, $trailId, $date)
->where('id', '!=', $this->route('climb')->id);
}
}
Result
- StoreRequest: 60+ lines (full logic)
- UpdateRequest: 11 lines (just the difference)
- Zero duplication
- Easy to maintain – change validation in one place
When to use this pattern
- Store and Update share the same validation rules
- The only difference is excluding the current record from uniqueness checks
- Custom validation logic in
after()hooks
Leave a Reply