Context
Introduction
Laravel's "context" capabilities enable you to capture, retrieve, and share information throughout requests, jobs, and commands executing within your application. This captured information is also included in logs written by your application, giving you deeper insight into the surrounding code execution history that occurred before a log entry was written and allowing you to trace execution flows throughout a distributed system.
How it Works
The best way to understand Laravel's context capabilities is to see it in action using the built-in logging features. To get started, you may add information to the context using the Context
facade. In this example, we will use a middleware to add the request URL and a unique trace ID to the context on every incoming request:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class AddContext
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());
return $next($request);
}
}
Information added to the context is automatically appended as metadata to any log entries that are written throughout the request. Appending context as metadata allows information passed to individual log entries to be differentiated from the information shared via Context
. For example, imagine we write the following log entry:
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
The written log will contain the auth_id
passed to the log entry, but it will also contain the context's url
and trace_id
as metadata:
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
Information added to the context is also made available to jobs dispatched to the queue. For example, imagine we dispatch a ProcessPodcast
job to the queue after adding some information to the context:
// In our middleware...
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());
// In our controller...
ProcessPodcast::dispatch($podcast);
When the job is dispatched, any information currently stored in the context is captured and shared with the job. The captured information is then hydrated back into the current context while the job is executing. So, if our job's handle method was to write to the log:
class ProcessPodcast implements ShouldQueue
{
use Queueable;
// ...
/**
* Execute the job.
*/
public function handle(): void
{
Log::info('Processing podcast.', [
'podcast_id' => $this->podcast->id,
]);
// ...
}
}
The resulting log entry would contain the information that was added to the context during the request that originally dispatched the job:
Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
Although we have focused on the built-in logging related features of Laravel's context, the following documentation will illustrate how context allows you to share information across the HTTP request / queued job boundary and even how to add hidden context data that is not written with log entries.
Capturing Context
You may store information in the current context using the Context
facade's add
method:
use Illuminate\Support\Facades\Context;
Context::add('key', 'value');
To add multiple items at once, you may pass an associative array to the add
method:
Context::add([
'first_key' => 'value',
'second_key' => 'value',
]);
The add
method will override any existing value that shares the same key. If you only wish to add information to the context if the key does not already exist, you may use the addIf
method:
Context::add('key', 'first');
Context::get('key');
// "first"
Context::addIf('key', 'second');
Context::get('key');
// "first"
Conditional Context
The when
method may be used to add data to the context based on a given condition. The first closure provided to the when
method will be invoked if the given condition evaluates to true
, while the second closure will be invoked if the condition evaluates to false
:
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Context;
Context::when(
Auth::user()->isAdmin(),
fn ($context) => $context->add('permissions', Auth::user()->permissions),
fn ($context) => $context->add('permissions', []),
);
Stacks
Context offers the ability to create "stacks", which are lists of data stored in the order that they were added. You can add information to a stack by invoking the push
method:
use Illuminate\Support\Facades\Context;
Context::push('breadcrumbs', 'first_value');
Context::push('breadcrumbs', 'second_value', 'third_value');
Context::get('breadcrumbs');
// [
// 'first_value',
// 'second_value',
// 'third_value',
// ]
Stacks can be useful to capture historical information about a request, such as events that are happening throughout your application. For example, you could create an event listener to push to a stack every time a query is executed, capturing the query SQL and duration as a tuple:
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;
DB::listen(function ($event) {
Context::push('queries', [$event->time, $event->sql]);
});