跳转至内容

上下文 (Context)

简介

Laravel 的“上下文 (Context)”功能使您能够在应用程序中执行的请求、作业和命令中捕获、检索和共享信息。这些捕获的信息还会包含在应用程序写入的日志中,从而让您更深入地了解在写入日志条目之前发生的周围代码执行历史,并允许您跟踪整个分布式系统中的执行流程。

工作原理

了解 Laravel 上下文功能的最佳方式是通过内置的日志功能查看其实际应用。首先,您可以使用 Context 门面 添加信息到上下文。在此示例中,我们将使用 中间件 在每个传入的请求上将请求 URL 和唯一跟踪 ID 添加到上下文中

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Illuminate\Support\Facades\Context;
8use Illuminate\Support\Str;
9use Symfony\Component\HttpFoundation\Response;
10 
11class AddContext
12{
13 /**
14 * Handle an incoming request.
15 */
16 public function handle(Request $request, Closure $next): Response
17 {
18 Context::add('url', $request->url());
19 Context::add('trace_id', Str::uuid()->toString());
20 
21 return $next($request);
22 }
23}

添加到上下文的信息会自动作为元数据附加到在请求期间写入的任何 日志条目 中。将上下文作为元数据附加,可以将传递给单个日志条目的信息与通过 Context 共享的信息区分开来。例如,假设我们编写以下日志条目

1Log::info('User authenticated.', ['auth_id' => Auth::id()]);

写入的日志将包含传递给日志条目的 auth_id,但它也会包含上下文的 urltrace_id 作为元数据

1User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

添加到上下文的信息也可用于分发到队列的作业。例如,假设我们在将一些信息添加到上下文后,分发了一个 ProcessPodcast 作业到队列

1// In our middleware...
2Context::add('url', $request->url());
3Context::add('trace_id', Str::uuid()->toString());
4 
5// In our controller...
6ProcessPodcast::dispatch($podcast);

当作业被分发时,当前存储在上下文中的任何信息都会被捕获并与作业共享。当作业执行时,捕获的信息会被重新水合 (hydrated) 到当前上下文中。因此,如果我们的作业的 handle 方法写入日志

1class ProcessPodcast implements ShouldQueue
2{
3 use Queueable;
4 
5 // ...
6 
7 /**
8 * Execute the job.
9 */
10 public function handle(): void
11 {
12 Log::info('Processing podcast.', [
13 'podcast_id' => $this->podcast->id,
14 ]);
15 
16 // ...
17 }
18}

生成的日志条目将包含在最初分发该作业的请求期间添加到上下文中的信息

1Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

尽管我们重点介绍了 Laravel 上下文中与日志相关的内置功能,但以下文档将说明上下文如何允许您跨 HTTP 请求/队列作业边界共享信息,甚至如何添加 隐藏上下文数据(这些数据不会随日志条目一起写入)。

捕获上下文

您可以使用 Context 门面的 add 方法将信息存储在当前上下文中

1use Illuminate\Support\Facades\Context;
2 
3Context::add('key', 'value');

要同时添加多个项目,您可以将关联数组传递给 add 方法

1Context::add([
2 'first_key' => 'value',
3 'second_key' => 'value',
4]);

add 方法将覆盖任何共享相同键的现有值。如果您只想在键尚不存在时才将信息添加到上下文中,可以使用 addIf 方法

1Context::add('key', 'first');
2 
3Context::get('key');
4// "first"
5 
6Context::addIf('key', 'second');
7 
8Context::get('key');
9// "first"

上下文还提供了用于递增或递减给定键的便捷方法。这两个方法都接受至少一个参数:要跟踪的键。可以提供第二个参数来指定键应递增或递减的量

1Context::increment('records_added');
2Context::increment('records_added', 5);
3 
4Context::decrement('records_added');
5Context::decrement('records_added', 5);

条件上下文

when 方法可用于根据给定条件将数据添加到上下文中。如果给定条件评估为 true,则会调用提供给 when 方法的第一个闭包;如果条件评估为 false,则会调用第二个闭包

1use Illuminate\Support\Facades\Auth;
2use Illuminate\Support\Facades\Context;
3 
4Context::when(
5 Auth::user()->isAdmin(),
6 fn ($context) => $context->add('permissions', Auth::user()->permissions),
7 fn ($context) => $context->add('permissions', []),
8);

作用域上下文

scope 方法提供了一种在给定回调执行期间临时修改上下文,并在回调执行完成时将上下文恢复到原始状态的方法。此外,您还可以传递在闭包执行时应合并到上下文中的额外数据(作为第二个和第三个参数)。

1use Illuminate\Support\Facades\Context;
2use Illuminate\Support\Facades\Log;
3 
4Context::add('trace_id', 'abc-999');
5Context::addHidden('user_id', 123);
6 
7Context::scope(
8 function () {
9 Context::add('action', 'adding_friend');
10 
11 $userId = Context::getHidden('user_id');
12 
13 Log::debug("Adding user [{$userId}] to friends list.");
14 // Adding user [987] to friends list. {"trace_id":"abc-999","user_name":"taylor_otwell","action":"adding_friend"}
15 },
16 data: ['user_name' => 'taylor_otwell'],
17 hidden: ['user_id' => 987],
18);
19 
20Context::all();
21// [
22// 'trace_id' => 'abc-999',
23// ]
24 
25Context::allHidden();
26// [
27// 'user_id' => 123,
28// ]

如果在作用域闭包内修改了上下文中的对象,则该更改将反映在作用域之外。

栈 (Stacks)

上下文提供了创建“栈”的能力,栈是按添加顺序存储的数据列表。您可以通过调用 push 方法将信息添加到栈中

1use Illuminate\Support\Facades\Context;
2 
3Context::push('breadcrumbs', 'first_value');
4 
5Context::push('breadcrumbs', 'second_value', 'third_value');
6 
7Context::get('breadcrumbs');
8// [
9// 'first_value',
10// 'second_value',
11// 'third_value',
12// ]

栈对于捕获有关请求的历史信息非常有用,例如在应用程序中发生的事件。例如,您可以创建一个事件监听器,在每次执行查询时推送到栈中,将查询 SQL 和持续时间捕获为元组

1use Illuminate\Support\Facades\Context;
2use Illuminate\Support\Facades\DB;
3 
4// In AppServiceProvider.php...
5DB::listen(function ($event) {
6 Context::push('queries', [$event->time, $event->sql]);
7});

您可以使用 stackContainshiddenStackContains 方法确定值是否在栈中

1if (Context::stackContains('breadcrumbs', 'first_value')) {
2 //
3}
4 
5if (Context::hiddenStackContains('secrets', 'first_value')) {
6 //
7}

stackContainshiddenStackContains 方法还接受闭包作为其第二个参数,从而允许对值比较操作进行更多控制

1use Illuminate\Support\Facades\Context;
2use Illuminate\Support\Str;
3 
4return Context::stackContains('breadcrumbs', function ($value) {
5 return Str::startsWith($value, 'query_');
6});

检索上下文

您可以使用 Context 门面的 get 方法从上下文中检索信息

1use Illuminate\Support\Facades\Context;
2 
3$value = Context::get('key');

onlyexcept 方法可用于检索上下文信息的一个子集

1$data = Context::only(['first_key', 'second_key']);
2 
3$data = Context::except(['first_key']);

pull 方法可用于从上下文中检索信息并立即将其从上下文中删除

1$value = Context::pull('key');

如果上下文数据存储在 中,您可以使用 pop 方法从栈中弹出项目

1Context::push('breadcrumbs', 'first_value', 'second_value');
2 
3Context::pop('breadcrumbs');
4// second_value
5 
6Context::get('breadcrumbs');
7// ['first_value']

rememberrememberHidden 方法可用于从上下文中检索信息,同时在所请求的信息不存在时,将上下文值设置为给定闭包返回的值

1$permissions = Context::remember(
2 'user-permissions',
3 fn () => $user->permissions,
4);

如果您想检索存储在上下文中的所有信息,可以调用 all 方法

1$data = Context::all();

判断项目是否存在

您可以使用 hasmissing 方法来确定上下文是否为给定键存储了任何值

1use Illuminate\Support\Facades\Context;
2 
3if (Context::has('key')) {
4 // ...
5}
6 
7if (Context::missing('key')) {
8 // ...
9}

has 方法将返回 true,无论存储的值是什么。因此,例如,值为 null 的键将被视为存在

1Context::add('key', null);
2 
3Context::has('key');
4// true

移除上下文

forget 方法可用于从当前上下文中删除一个键及其值

1use Illuminate\Support\Facades\Context;
2 
3Context::add(['first_key' => 1, 'second_key' => 2]);
4 
5Context::forget('first_key');
6 
7Context::all();
8 
9// ['second_key' => 2]

您可以通过向 forget 方法提供数组来一次忘记多个键

1Context::forget(['first_key', 'second_key']);

隐藏上下文

上下文提供了存储“隐藏”数据的能力。此隐藏信息不会附加到日志中,并且无法通过上述记录的数据检索方法访问。上下文提供了一组不同的方法来与隐藏上下文信息进行交互

1use Illuminate\Support\Facades\Context;
2 
3Context::addHidden('key', 'value');
4 
5Context::getHidden('key');
6// 'value'
7 
8Context::get('key');
9// null

“隐藏”方法反映了上述记录的非隐藏方法的功能

1Context::addHidden(/* ... */);
2Context::addHiddenIf(/* ... */);
3Context::pushHidden(/* ... */);
4Context::getHidden(/* ... */);
5Context::pullHidden(/* ... */);
6Context::popHidden(/* ... */);
7Context::onlyHidden(/* ... */);
8Context::exceptHidden(/* ... */);
9Context::allHidden(/* ... */);
10Context::hasHidden(/* ... */);
11Context::missingHidden(/* ... */);
12Context::forgetHidden(/* ... */);

活动

上下文分发了两个事件,允许您挂载到上下文的水合和脱水过程中。

为了说明如何使用这些事件,假设在应用程序的中间件中,您根据传入 HTTP 请求的 Accept-Language 标头设置了 app.locale 配置值。上下文的事件允许您在请求期间捕获此值并在队列上恢复它,确保在队列上发送的通知具有正确的 app.locale 值。我们可以使用上下文的事件和 隐藏 数据来实现这一点,下文将对此进行说明。

脱水 (Dehydrating)

每当作业被分发到队列时,上下文中的数据就会被“脱水 (dehydrated)”并与作业的有效负载一起捕获。Context::dehydrating 方法允许您注册一个闭包,该闭包将在脱水过程中被调用。在此闭包内,您可以更改将与队列作业共享的数据。

通常,您应该在应用程序 AppServiceProvider 类的 boot 方法中注册 dehydrating 回调

1use Illuminate\Log\Context\Repository;
2use Illuminate\Support\Facades\Config;
3use Illuminate\Support\Facades\Context;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Context::dehydrating(function (Repository $context) {
11 $context->addHidden('locale', Config::get('app.locale'));
12 });
13}

您不应在 dehydrating 回调中使用 Context 门面,因为这会改变当前进程的上下文。确保仅对传递给回调的存储库进行更改。

水合 (Hydrated)

每当队列作业开始在队列上执行时,与该作业共享的任何上下文都将“水合 (hydrated)”回当前上下文。Context::hydrated 方法允许您注册一个闭包,该闭包将在水合过程中被调用。

通常,您应该在应用程序 AppServiceProvider 类的 boot 方法中注册 hydrated 回调

1use Illuminate\Log\Context\Repository;
2use Illuminate\Support\Facades\Config;
3use Illuminate\Support\Facades\Context;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Context::hydrated(function (Repository $context) {
11 if ($context->hasHidden('locale')) {
12 Config::set('app.locale', $context->getHidden('locale'));
13 }
14 });
15}

您不应在 hydrated 回调中使用 Context 门面,并确保仅对传递给回调的存储库进行更改。