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 Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// ...
/**
* 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
where 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]);
});
Retrieving Context
You may retrieve information from the context using the
Context
facade's get
method:
use Illuminate\Support\Facades\Context;
$value = Context::get('key');
The only
method may be used to retrieve a
subset of the information in the context:
$data = Context::only(['first_key', 'second_key']);
If you would like to retrieve all of the information
stored in the context, you may invoke the
all
method:
$data = Context::all();
Determining Item Existence
You may use the has
method to determine if
the context has any value stored for the given key:
use Illuminate\Support\Facades\Context;
if (Context::has('key')) {
// ...
}
The has
method will return true
regardless of the value stored. So, for example, a key
with a null
value will be considered
present:
Context::add('key', null);
Context::has('key');
// true
Removing Context
The forget
method may be used to remove a
key and its value from the current context:
use Illuminate\Support\Facades\Context;
Context::add(['first_key' => 1, 'second_key' => 2]);
Context::forget('first_key');
Context::all();
// ['second_key' => 2]
You may forget several keys at once by providing an array
to the forget
method:
Context::forget(['first_key', 'second_key']);
Hidden Context
Context offers the ability to store "hidden" data. This hidden information is not appended to logs, and is not accessible via the data retrieval methods documented above. Context provides a different set of methods to interact with hidden context information:
use Illuminate\Support\Facades\Context;
Context::addHidden('key', 'value');
Context::getHidden('key');
// 'value'
Context::get('key');
// null
The "hidden" methods mirror the functionality of the non-hidden methods documented above:
Context::addHidden(/* ... */);
Context::addHiddenIf(/* ... */);
Context::pushHidden(/* ... */);
Context::getHidden(/* ... */);
Context::onlyHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::forgetHidden(/* ... */);
Events
Context dispatches two events that allow you to hook into the hydration and dehydration process of the context.
To illustrate how these events may be used, imagine that
in a middleware of your application you set the
app.locale
configuration value based on the
incoming HTTP request's Accept-Language
header. Context's events allow you to capture this value
during the request and restore it on the queue, ensuring
notifications sent on the queue have the correct
app.locale
value. We can use context's
events and hidden data to
achieve this, which the following documentation will
illustrate.
Dehydrating
Whenever a job is dispatched to the queue the data in the
context is "dehydrated" and captured alongside
the job's payload. The Context::dehydrating
method allows you to register a closure that will be
invoked during the dehydration process. Within this
closure, you may make changes to the data that will be
shared with the queued job.
Typically, you should register dehydrating
callbacks within the boot
method of your
application's AppServiceProvider
class:
use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Context::dehydrating(function (Repository $context) {
$context->addHidden('locale', Config::get('app.locale'));
});
}
Note: You should not use the
Context
facade within thedehydrating
callback, as that will change the context of the current process. Ensure you only make changes to the repository passed to the callback.
Hydrated
Whenever a queued job begins executing on the queue, any
context that was shared with the job will be
"hydrated" back into the current context. The
Context::hydrated
method allows you to
register a closure that will be invoked during the
hydration process.
Typically, you should register hydrated
callbacks within the boot
method of your
application's AppServiceProvider
class:
use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Context::hydrated(function (Repository $context) {
if ($context->hasHidden('locale')) {
Config::set('app.locale', $context->getHidden('locale'));
}
});
}
Note: You should not use the
Context
facade within thehydrated
callback and instead ensure you only make changes to the repository passed to the callback.