Skip to content

Instantly share code, notes, and snippets.

@karkowg
Last active May 23, 2025 14:13
Show Gist options
  • Select an option

  • Save karkowg/1ffaa13d4a96583e3a137c8a89bf90c2 to your computer and use it in GitHub Desktop.

Select an option

Save karkowg/1ffaa13d4a96583e3a137c8a89bf90c2 to your computer and use it in GitHub Desktop.
Scoping Eloquent Relations Using the Context Facade
title Scoping Eloquent Relations Using the Context Facade
date 2025-05-09

Scoping Eloquent Relations Using the Context Facade

Introduction

When working with Laravel's Eloquent ORM, you may need to access related models based on specific conditions that are only known at runtime. The traditional approach involves querying a relationship with additional constraints, which can become repetitive and less elegant when the same constraint is needed across multiple parts of your application.

In this article, we'll explore how to use Laravel's Context facade to elegantly scope Eloquent relations at runtime. This technique allows you to define relationships that automatically filter based on contextual information, leading to cleaner, more maintainable code.

The Problem: Accessing Context-Specific Relations

Let's consider a common problem space: an event management system where users can attend multiple events. Each user has many event attendees.

When handling a request for a specific event, you often need to access the user's attendance record for just that event. The traditional approach might look something like this:

// Get the user's attendance record for a specific event
$eventAttendee = $user->eventAttendees()
    ->where('event_id', $event->id)
    ->first();

if ($eventAttendee?->registered) {
    // Do something if the user is registered for this event
}

While this works, it's not ideal for a few different reasons:

  1. You have to write this filtering logic everywhere you need to access the user's attendance for a specific event.
  2. It's verbose and repetitive.
  3. You can't simply use $user->eventAttendee.
  4. You can't eager load the relation using User::with('eventAttendee') or $user->load('eventAttendee').

Enter the Context Facade

Laravel's Context facade provides a solution. It allows you to store and retrieve contextual information during the request lifecycle, which your Eloquent relationships can then use to apply dynamic constraints. Let's see this in action with our event attendee example. We could store the current event ID using the Context facade and define a relationship that uses this contextual information to scope itself.

Implementation Example

First, let's define our routes and middleware:

// In routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/events/{event}', EventController::class)
        ->middleware(RedirectIfMissingRegistration::class);

    Route::get('/events/{event}/register', RegistrationController::class)
        ->name('event.registration');
});

Now, our middleware that adds context and checks registration status:

class RedirectIfMissingRegistration
{
    public function handle(Request $request, Closure $next): Response
    {
        /** @var \App\Models\Event $event */
        $event = $request->route('event');

        // Add event ID to the context
        Context::addHidden('event_id', $event->id);

        if ($request->user()->eventAttendee?->registered) {
            return $next($request);
        }

        return redirect()->route('event.registration', [$event]);
    }
}

And finally, our User model with the contextually scoped relationship:

class User
{
    public function eventAttendees(): HasMany
    {
        return $this->hasMany(EventAttendee::class);
    }

    public function eventAttendee(): HasOne
    {
        return $this->eventAttendees()
            ->one()
            ->ofMany(['id' => 'max'], function ($query) {
                $query->where('event_id', Context::getHidden('event_id'));
            });
    }
}

Let's break down what's happening:

1. Setting Context in Middleware

In the RedirectIfMissingRegistration middleware, we:

  • Extract the event instance from the route.
  • Store the event ID in the Context using Context::addHidden('event_id', $event->id).
  • Use the contextually scoped eventAttendee relationship to check if the user is registered.

2. Defining the Contextual Relationship

In the User model, we define two relationships:

  • eventAttendees(): A standard hasMany relationship to all event attendee records.
  • eventAttendee(): A contextually scoped relationship that:
    • Builds on the eventAttendees relationship.
    • Uses one()->ofMany() to select a single record.
    • Applies a constraint based on the event ID from Context.

Taking it Further: Hooking into the User Provider

Building on the previous implementation, we can take this pattern further by integrating it directly into how Laravel resolves the authenticated user.

class AuthServiceProvider extends ServiceProvider
{
    public function boot() : void
    {
        Auth::provider('eloquent', function (Application $app, array $config) {
            $provider = new EloquentUserProvider($app['hash'], $config['model']);

            return $provider->withQuery(function (Builder $query) {
                $query->when(request()->routeIs('event.*'), function (Builder $query) {
                    /** @var \App\Models\Event $event */
                    $event = request()->route('event');

                    Context::addHidden('event_id', $event->id);

                    $query->with('eventAttendee');
                });
            });
        });
    }
}

In this approach, we're extending Laravel's authentication system by customizing how the 'eloquent' user provider works. When a request matches an event-related route pattern:

  1. It adds the event ID to the Context.
  2. It eager loads the eventAttendee relation whenever the user is first retrieved.

We're essentially achieving:

  1. Magic: Your code can simply use $user->eventAttendee without having to filter the hasMany relation.
  2. Automatic relation loading: The eventAttendee relation is always eager loaded on relevant routes, improving performance.
  3. Optimized database queries: Whenever you access the authenticated user through auth()->user() or $request->user(), the eventAttendee relation is already eager loaded. This means there's no additional query when you access $user->eventAttendee. If your application accesses this relationship in multiple middlewares, controllers, or service classes, this could potentially reduce the number of database queries executed during a request.

Benefits

With this approach, we can now directly access $user->eventAttendee anywhere in our code, and it will automatically be scoped to the current event. This provides several benefits:

  1. Natural relationship syntax: You can use the typical $model->relation property access that Laravel developers expect and appreciate.
  2. Eager loading support: It works seamlessly with with('eventAttendee') for efficient database queries.
  3. Request-specific by design: The Context facade is purpose-built for request lifecycle data.
  4. Centralized definition: The relationship logic is defined once in the model, promoting DRY principles, if you're into that.

Additional Considerations

When using Context for scoping Eloquent relationships, there are a few important things to keep in mind:

  1. Context Lifecycle: Context data exists only for the duration of the request. Make sure to set the context early enough in the request lifecycle (typically in a middleware) for any code that needs it.
  2. Clear Intentions: Using Context makes your code more elegant but potentially less explicit. Make sure to document your approach so other developers understand that the relationship depends on contextual data. In our example, we could rename the relation to eventAttendeeForCurrentContext to make it clearer.
  3. Performance: Since Context uses in-memory storage, it's very efficient compared to alternatives like session1 or cache.

Conclusion

Using Laravel's Context facade to scope Eloquent relations at runtime provides an elegant solution for accessing context-specific relationships. It preserves the familiar Eloquent relationship patterns while adding runtime flexibility, leading to cleaner, more maintainable code.

This technique is particularly valuable in applications where you frequently need to access related models based on runtime context, such as multi-tenant applications, user-specific content filtering, or - as in our example - event-specific user data.

By leveraging the Context facade, you can write more expressive code that follows Laravel's elegant syntax patterns while still accommodating dynamic, request-specific filtering requirements.

Thanks for reading! If you have any feedback, I'm @gksh.dev on bluesky.

Footnotes

  1. One might reach for Laravel's session to store context-specific values. However, sessions are designed to persist across multiple requests, which can lead to stale context data if not carefully managed. Furthermore, session access often involves disk or database operations, unlike Context's in-memory approach. It also doesn't bridge the request-queue boundary like Context does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment