Amitav Roy

The Query I Was Running Twice: How Laravel's Context Solved My Validation Dilemma


Published on: 10th Jan, 2026 by Amitav Roy
Stop duplicating database queries between validation and controller logic. Laravel's Context facade, combined with withValidator, lets you validate, load, and share models in one elegant flow.

It started during a live coding session on my YouTube channel.

I was building a feature, writing the familiar pattern—exists validation in the Request class, findOrFail in the controller—when I paused and mentioned something that had been bothering me for a while.

"I know this is hitting the database twice for the same record," I said to the stream. "Once in the Request class to validate it exists, and again in the controller to actually use it. I need to find a better way to handle this."

That's when a friend watching the stream dropped a comment that changed how I write validation logic. He introduced me to combining Laravel's Context facade with withValidator—a pattern I'd never considered.

The Pattern That Felt Wrong

Here's the scenario every Laravel developer knows. You have a form request that needs to validate whether a related record exists:

class UpdateProjectTaskRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'project_id' => 'required|exists:projects,id',
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
        ];
    }
}

Clean. Readable. Laravel's exists rule handles the validation beautifully. But here's what happens in the controller:

public function store(UpdateProjectTaskRequest $request)
{
    $project = Project::findOrFail($request->project_id);
    
    $task = $project->tasks()->create($request->validated());
    
    return response()->json($task, 201);
}

See the problem? We've already confirmed the project exists during validation—Laravel hit the database to verify that. Now we're hitting the database again to retrieve the same record. Two queries for one piece of information.

For years, I worked around this by skipping the exists validation entirely:

// My "optimization" - skip validation, use findOrFail
public function rules(): array
{
    return [
        'project_id' => 'required|integer',
        'title' => 'required|string|max:255',
    ];
}

Then in the controller, I'd let findOrFail handle both validation and retrieval. It felt clever. It was also wrong.

I was trading clean architecture for a micro-optimization that probably didn't matter—at least not in small applications. But in systems handling hundreds or thousands of concurrent users, those redundant queries add up. Every unnecessary database hit contributes to connection pool pressure, increased latency, and reduced throughput under load.

Beyond performance, the architectural cost was immediate regardless of scale. This approach scattered validation logic between the Request class and the controller. The Request class no longer told the complete story of what constituted valid input. Business rules leaked into controller methods. Code became harder to reason about.

The original problem—that redundant query—kept nagging at me. I knew there had to be a cleaner way.

The Discovery: withValidator Meets Context

Right there in the live stream, my friend explained the approach he'd been using. He mentioned Laravel's Context facade and the withValidator method—a combination that solved exactly what I was struggling with.

Context lets you store data that persists through the entire request lifecycle. Data added in middleware is available in controllers. Data added in Request classes is available everywhere downstream.

The idea clicked immediately: validate the record exists, load it in the same query, and make it available to the controller—all from within the Request class.

Enter withValidator:

class UpdateProjectTaskRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'project_id' => 'required|integer',
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $validator) {
            if ($validator->errors()->isNotEmpty()) {
                return;
            }

            $project = Project::find($this->project_id);

            if (!$project) {
                $validator->errors()->add(
                    'project_id',
                    'The selected project does not exist.'
                );
                return;
            }

            Context::add('project', $project);
        });
    }
}

The withValidator method gives you access to the validator instance after the standard rules have been evaluated. The after callback runs once those rules pass. This is where you add custom validation logic—logic that can do more than just validate. Notice what's happening: I find the project, validate it exists, and immediately store it in Context. One query. Full validation. Model ready for use.

Now the controller becomes:

public function store(UpdateProjectTaskRequest $request)
{
    $project = Context::get('project');
    
    $task = $project->tasks()->create($request->validated());
    
    return response()->json($task, 201);
}

No findOrFail. No redundant query. The project is already loaded and validated—we just retrieve it from Context.

Why This Matters More Than You Think

"It's just one query," you might say. "Database queries are fast."

True. But consider the compound effect:

That endpoint gets called hundreds of times per hour. Each redundant query adds milliseconds. Those milliseconds add up to seconds of unnecessary database time daily. Under load, those extra queries contribute to connection pool exhaustion.

More importantly, this pattern scales. Complex forms often validate multiple related records:

public function withValidator(Validator $validator): void
{
    $validator->after(function (Validator $validator) {
        if ($validator->errors()->isNotEmpty()) {
            return;
        }

        $project = Project::with(['team', 'settings'])->find($this->project_id);

        if (!$project) {
            $validator->errors()->add('project_id', 'Project not found.');
            return;
        }

        if (!$project->team->members->contains(auth()->user())) {
            $validator->errors()->add('project_id', 'You do not have access to this project.');
            return;
        }

        if (!$project->settings->allows_task_creation) {
            $validator->errors()->add('project_id', 'Task creation is disabled for this project.');
            return;
        }

        Context::add('project', $project);
    });
}

Now you're validating existence, authorization, and business rules—all while loading the model once. The controller receives a fully validated, pre-loaded entity ready for use.

The Architecture Win

Beyond performance, this pattern improves code organization in meaningful ways.

The Request class becomes the single source of truth for input validation. Everything that determines whether a request is valid lives in one place. No more hunting through controllers to understand validation logic.

Controllers become thinner. They focus on orchestrating business logic, not validating input or loading records that should already be validated and loaded.

Testing becomes clearer. You can test validation logic in isolation by testing the Request class. You can test controller logic knowing that the Request class has already ensured valid input.

When Not to Use This Pattern

This approach isn't universal. Consider the tradeoffs:

If you need the model regardless of validation—say, for logging or error tracking even when validation fails—loading it in the controller might make more sense.

If your validation logic is simple and the exists rule suffices, the extra complexity of withValidator isn't worth it. Don't over-engineer straightforward cases.

If the model requires complex loading (many relations, scopes, etc.) that varies by controller action, centralizing that loading in the Request class might not fit.

Use judgment. The goal is cleaner, more efficient code—not rigid adherence to patterns.

The Broader Lesson

This discovery reminded me of something I've learned repeatedly over 16 years of building software: small inefficiencies compound. That "just one extra query" multiplied across endpoints, requests, and users becomes a meaningful performance drag.

More valuable than the specific technique is the habit of questioning established patterns. I'd written validation-then-fetch code for years without examining the assumption that they had to be separate concerns. The tools to unify them existed—I just hadn't looked.

Laravel's Context facade, withValidator, custom validation rules—these aren't exotic features. They're documented, stable, and widely used. But they require intentional exploration to discover how they solve problems you didn't know you had.

The next time you write exists in a validation rule and findOrFail in the controller, pause. Ask whether you're running the same query twice. The answer might lead you somewhere useful.

Quick Reference

Here's the complete pattern for reference:

<?php

namespace App\Http\Requests;

use App\Models\Project;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Context;
use Illuminate\Validation\Validator;

class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'project_id' => 'required|integer',
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'due_date' => 'nullable|date|after:today',
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $validator) {
            // Skip if basic validation already failed
            if ($validator->errors()->isNotEmpty()) {
                return;
            }

            $project = Project::find($this->project_id);

            if (!$project) {
                $validator->errors()->add(
                    'project_id',
                    'The selected project does not exist.'
                );
                return;
            }

            // Add to Context for controller access
            Context::add('project', $project);
        });
    }
}

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StoreTaskRequest;
use Illuminate\Support\Facades\Context;

class TaskController extends Controller
{
    public function store(StoreTaskRequest $request)
    {
        // Project already validated and loaded
        $project = Context::get('project');

        $task = $project->tasks()->create($request->validated());

        return response()->json($task, 201);
    }
}

Build systems that don't repeat themselves. Your database will thank you.