If you've worked with Laravel's Form Requests, you've probably run into the after() method for custom validation. But it works slightly differently that it named.
Common Misconception
When you see a method called after(), you'd figure it runs after all the regular validation passes. That's what I assumed, too. Makes sense, doesn't it? But that's not how it works.
Real Example
Let me show you what I mean with a real example. Here's how you might set up validation rules for storing related articles in catalog:
public function rules(): array
{
return [
'article' => ['required', ...DetailArticleRule::rules()],
'brand_id' => ['required', 'integer', 'min:1'],
'related_article' => ['required', ...DetailArticleRule::rules(), 'different:article'],
];
}
public function after(): array
{
return [
function (Validator $validator): void {
$brandId = $this->integer('brand_id');
$existsMain = Product::query()
->where('article', $this->string('article')->toString())
->where('brand_id', $brandId)
->exists();
if (!$existsMain) {
$validator->errors()->add('article', __('validation.exists'));
}
// ... more checks
},
];
}
At first glance, this makes sense. You do your usual validation, then use after() to tack on extra database checks.
The Problem
Ok, try sending "bad" request:
{
"article": "P900RM11",
"brand_id": "Hepu",
"related_article": "P900RM11005"
}
You get a nice error response about invalid brand_id (it's string, not int) and bad articles.
But take a look at your database logs:
select exists(select * from `products` where `article` = 'P900RM11005'...)
select exists(select * from `products` where `article` = 'P900RM11'...)
Wait a second, validation failed, but you're still hitting the database. Not cool.
And it gets messier. What if someone sends this?
{
"article": "P900RM11",
"brand_id": 5,
"related_article": ["P900RM11005"]
}
Now you get an "Array to string conversion" exception. The after() callback tries to process invalid types, falls over, and you end up with a 500 error instead of a helpful validation message.
What Laravel Docs Actually Say
From the off docs:
The after method accepts a closure or an array of callables which will be invoked after validation is complete.
The important bit is "after validation is complete" - not "after validation passes." That means after() runs even when validation fails. Yeah, the name is misleading.
The Quick Fix
So, if you're using after(), you need to guard yourself:
public function after(): array
{
return [
function (Validator $validator): void {
if ($validator->errors()->isNotEmpty()) {
return; // Stop if basic validation failed
}
// Now it's safe to do DB checks
},
];
}
This way, you avoid unnecessary queries and weird crashes.
The Better Solution
But here's the bigger point: database queries don't belong in HTTP validation. Validation should be fast and stateless. Its job is to check the shape of the data and basic rules-nothing that requires hitting the database.
Mixing in database calls here slows things down, couples your HTTP layer to your persistence layer, and makes testing a pain. Plus, you burn resources on requests that weren't valid from the start.
The better approach? Move this stuff into a service layer:
// In RelatedArticleService
public function createEdge(string $article, int $brandId, string $relatedArticle, int $relatedBrandId): void
{
$this->validateEdge($article, $brandId, $relatedArticle, $relatedBrandId);
$this->repository->insertEdge($article, $brandId, $relatedArticle, $relatedBrandId);
// more business logic...
}
private function validateEdge(string $article, int $brandId, string $relatedArticle, int $relatedBrandId): void
{
$existsMain = Product::query()
->where('article', $article)
->where('brand_id', $brandId)
->exists();
if (!$existsMain) {
throw ValidationException::withMessages([
'article' => __('validation.exists', ['attribute' => 'article'])
]);
}
// other checks...
}
Now, your Form Request just handles HTTP validation, and your service takes care of all the business logic and any domain-specific checks.
Final Thoughts
So, to sum it all up: Laravel's after() method can be useful, but keep these things in mind
- It runs no matter what - even if the main validation failed.
- If you want conditional logic, you'll need to check for errors yourself.
- Heavy operations (like database queries) belong in your service layer, not Form Requests.
That's really the best way to keep your code fast, clean, and easy to test.
Author's Note
Thanks for sticking around!
Find me on dev.to, linkedin, or you can check out my work on github.
Notes from real-world Laravel.
Top comments (0)