上下文
简介
Laravel 的“上下文”功能使您能够在应用程序中执行的请求、作业和命令中捕获、检索和共享信息。此捕获的信息也包含在应用程序写入的日志中,让您可以更深入地了解编写日志条目之前发生的周围代码执行历史,并允许您在分布式系统中跟踪执行流程。
工作原理
理解 Laravel 上下文功能的最佳方法是使用内置的日志记录功能来实际操作。要开始使用,您可以使用 Context
外观 向上下文中添加信息。在此示例中,我们将使用中间件在每个传入请求的上下文中添加请求 URL 和唯一的跟踪 ID
<?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); }}
添加到上下文的信息会自动作为元数据附加到整个请求中写入的任何日志条目。将上下文作为元数据附加,可以区分传递给各个日志条目的信息和通过 Context
共享的信息。例如,假设我们编写以下日志条目
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
写入的日志将包含传递给日志条目的 auth_id
,但它还将包含上下文的 url
和 trace_id
作为元数据
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
添加到上下文的信息也可用于调度到队列的作业。例如,假设我们在向上下文添加一些信息后,将 ProcessPodcast
作业调度到队列
// In our middleware...Context::add('url', $request->url());Context::add('trace_id', Str::uuid()->toString()); // In our controller...ProcessPodcast::dispatch($podcast);
当作业被调度时,当前存储在上下文中的任何信息都会被捕获并与作业共享。然后,捕获的信息在作业执行期间被水合回当前上下文。因此,如果我们的作业的 handle 方法要写入日志
class ProcessPodcast implements ShouldQueue{ use Queueable; // ... /** * Execute the job. */ public function handle(): void { Log::info('Processing podcast.', [ 'podcast_id' => $this->podcast->id, ]); // ... }}
生成的日志条目将包含在最初调度作业的请求期间添加到上下文中的信息
Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
尽管我们专注于 Laravel 上下文的内置日志记录相关功能,但以下文档将说明上下文如何允许您跨 HTTP 请求/排队作业边界共享信息,甚至如何添加不写入日志条目的隐藏上下文数据。
捕获上下文
您可以使用 Context
外观的 add
方法将信息存储在当前上下文中
use Illuminate\Support\Facades\Context; Context::add('key', 'value');
要一次添加多个项目,您可以将关联数组传递给 add
方法
Context::add([ 'first_key' => 'value', 'second_key' => 'value',]);
add
方法将覆盖任何共享同一键的现有值。如果您只想在键不存在时才将信息添加到上下文中,您可以使用 addIf
方法
Context::add('key', 'first'); Context::get('key');// "first" Context::addIf('key', 'second'); Context::get('key');// "first"
条件上下文
when
方法可用于根据给定的条件将数据添加到上下文中。如果给定的条件评估为 true
,则会调用提供给 when
方法的第一个闭包,而如果条件评估为 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', []),);
堆栈
上下文提供了创建“堆栈”的能力,这些“堆栈”是以添加顺序存储的数据列表。您可以通过调用 push
方法将信息添加到堆栈
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',// ]
堆栈可用于捕获有关请求的历史信息,例如整个应用程序中发生的事件。例如,您可以创建一个事件侦听器,以便在每次执行查询时将查询 SQL 和持续时间作为元组推送到堆栈
use Illuminate\Support\Facades\Context;use Illuminate\Support\Facades\DB; DB::listen(function ($event) { Context::push('queries', [$event->time, $event->sql]);});
您可以使用 stackContains
和 hiddenStackContains
方法来确定值是否在堆栈中
if (Context::stackContains('breadcrumbs', 'first_value')) { //} if (Context::hiddenStackContains('secrets', 'first_value')) { //}
stackContains
和 hiddenStackContains
方法也接受一个闭包作为其第二个参数,从而可以更好地控制值比较操作
use Illuminate\Support\Facades\Context;use Illuminate\Support\Str; return Context::stackContains('breadcrumbs', function ($value) { return Str::startsWith($value, 'query_');});
检索上下文
您可以使用 Context
外观的 get
方法从上下文中检索信息
use Illuminate\Support\Facades\Context; $value = Context::get('key');
only
方法可用于检索上下文中信息的子集
$data = Context::only(['first_key', 'second_key']);
pull
方法可用于从上下文中检索信息并立即将其从上下文中删除
$value = Context::pull('key');
如果上下文数据存储在堆栈中,则可以使用 pop
方法从堆栈中弹出项目
Context::push('breadcrumbs', 'first_value', 'second_value'); Context::pop('breadcrumbs')// second_value Context::get('breadcrumbs');// ['first_value']
如果您想检索存储在上下文中的所有信息,可以调用 all
方法
$data = Context::all();
确定项目是否存在
您可以使用 has
方法来确定上下文是否为给定的键存储了任何值
use Illuminate\Support\Facades\Context; if (Context::has('key')) { // ...}
无论存储的值如何,has
方法都将返回 true
。因此,例如,具有 null
值的键将被视为存在
Context::add('key', null); Context::has('key');// true
移除上下文
forget
方法可用于从当前上下文中删除键及其值
use Illuminate\Support\Facades\Context; Context::add(['first_key' => 1, 'second_key' => 2]); Context::forget('first_key'); Context::all(); // ['second_key' => 2]
您可以通过向 forget
方法提供一个数组来一次忘记多个键
Context::forget(['first_key', 'second_key']);
隐藏上下文
上下文提供了存储“隐藏”数据的能力。此隐藏信息不会附加到日志,并且无法通过上面记录的数据检索方法访问。上下文提供了一组不同的方法来与隐藏的上下文信息交互
use Illuminate\Support\Facades\Context; Context::addHidden('key', 'value'); Context::getHidden('key');// 'value' Context::get('key');// null
“隐藏”方法镜像了上面记录的非隐藏方法的功能
Context::addHidden(/* ... */);Context::addHiddenIf(/* ... */);Context::pushHidden(/* ... */);Context::getHidden(/* ... */);Context::pullHidden(/* ... */);Context::popHidden(/* ... */);Context::onlyHidden(/* ... */);Context::allHidden(/* ... */);Context::hasHidden(/* ... */);Context::forgetHidden(/* ... */);
事件
上下文调度两个事件,使您可以挂钩到上下文的水合和脱水过程。
为了说明如何使用这些事件,假设在应用程序的中间件中,您根据传入的 HTTP 请求的 Accept-Language
标头设置 app.locale
配置值。上下文的事件允许您在请求期间捕获此值并在队列上恢复它,从而确保在队列上发送的通知具有正确的 app.locale
值。我们可以使用上下文的事件和隐藏数据来实现此目的,以下文档将对此进行说明。
脱水
每当作业被调度到队列时,上下文中的数据都会“脱水”,并与作业的有效负载一起捕获。Context::dehydrating
方法允许您注册一个闭包,该闭包将在脱水过程中被调用。在此闭包中,您可以更改将与排队作业共享的数据。
通常,您应该在应用程序的 AppServiceProvider
类的 boot
方法中注册 dehydrating
回调
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')); });}
您不应在 dehydrating
回调中使用 Context
外观,因为这会更改当前进程的上下文。确保您仅对传递给回调的存储库进行更改。
水合
每当队列中的一个排队作业开始执行时,任何与该作业共享的上下文都将被“水合”回当前上下文。Context::hydrated
方法允许您注册一个闭包,该闭包将在水合过程中被调用。
通常,您应该在应用程序的 AppServiceProvider
类的 boot
方法中注册 hydrated
回调。
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')); } });}
您不应该在 hydrated
回调中使用 Context
外观,而应确保仅对传递给回调的存储库进行更改。