上下文
简介
Laravel 的 “上下文” 功能使您能够在应用程序内执行的请求、任务和命令中捕获、检索和共享信息。此捕获的信息也包含在应用程序编写的日志中,让您可以更深入地了解在写入日志条目之前发生的周围代码执行历史记录,并允许您在分布式系统中跟踪执行流程。
工作原理
理解 Laravel 上下文功能的最佳方式是使用内置的日志功能来观察它的实际应用。要开始使用,您可以将信息添加到上下文中,使用 Context
facade。在此示例中,我们将使用中间件在每个传入请求中将请求 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 AddContext12{13 /**14 * Handle an incoming request.15 */16 public function handle(Request $request, Closure $next): Response17 {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
,但它也将包含上下文的 url
和 trace_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);
当任务被分派时,当前存储在上下文中的任何信息都会被捕获并与任务共享。然后,捕获的信息在任务执行时被水合回当前上下文。因此,如果我们的任务的 handle 方法要写入日志
1class ProcessPodcast implements ShouldQueue 2{ 3 use Queueable; 4 5 // ... 6 7 /** 8 * Execute the job. 9 */10 public function handle(): void11 {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
facade 的 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"
条件上下文
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);
堆栈
上下文提供了创建 “堆栈” 的能力,堆栈是以添加顺序存储的数据列表。您可以通过调用 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 4DB::listen(function ($event) {5 Context::push('queries', [$event->time, $event->sql]);6});
您可以使用 stackContains
和 hiddenStackContains
方法确定值是否在堆栈中
1if (Context::stackContains('breadcrumbs', 'first_value')) {2 //3}4 5if (Context::hiddenStackContains('secrets', 'first_value')) {6 //7}
stackContains
和 hiddenStackContains
方法也接受闭包作为它们的第二个参数,从而可以更好地控制值比较操作
1use Illuminate\Support\Facades\Context;2use Illuminate\Support\Str;3 4return Context::stackContains('breadcrumbs', function ($value) {5 return Str::startsWith($value, 'query_');6});
检索上下文
您可以使用 Context
facade 的 get
方法从上下文中检索信息
1use Illuminate\Support\Facades\Context;2 3$value = Context::get('key');
only
方法可用于检索上下文中信息的子集
1$data = Context::only(['first_key', 'second_key']);
pull
方法可用于从上下文中检索信息并立即将其从上下文中删除
1$value = Context::pull('key');
如果上下文数据存储在堆栈中,您可以使用 pop
方法从堆栈中弹出项目
1Context::push('breadcrumbs', 'first_value', 'second_value');2 3Context::pop('breadcrumbs')4// second_value5 6Context::get('breadcrumbs');7// ['first_value']
如果您想检索存储在上下文中的所有信息,可以调用 all
方法
1$data = Context::all();
确定项目是否存在
您可以使用 has
和 missing
方法来确定上下文是否为给定的键存储了任何值
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::allHidden(/* ... */); 9Context::hasHidden(/* ... */);10Context::forgetHidden(/* ... */);
事件
上下文分派两个事件,允许您挂钩到上下文的水合和脱水过程。
为了说明如何使用这些事件,假设在应用程序的中间件中,您根据传入的 HTTP 请求的 Accept-Language
标头设置 app.locale
配置值。上下文的事件允许您在请求期间捕获此值并在队列中恢复它,确保在队列上发送的通知具有正确的 app.locale
值。我们可以使用上下文的事件和隐藏数据来实现这一点,以下文档将对此进行说明。
脱水
每当任务被分派到队列时,上下文中的数据都会被 “脱水” 并与任务的有效负载一起捕获。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
facade,因为这会更改当前进程的上下文。确保您只更改传递给回调的存储库。
水合
每当排队任务开始在队列上执行时,与任务共享的任何上下文都将 “水合” 回当前上下文。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
facade,而是确保您只更改传递给回调的存储库。