跳至内容

上下文

简介

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,但它还将包含上下文的 urltrace_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]);
});

你可以使用 stackContainshiddenStackContains 方法确定栈中是否存在某个值。

if (Context::stackContains('breadcrumbs', 'first_value')) {
//
}
 
if (Context::hiddenStackContains('secrets', 'first_value')) {
//
}

stackContainshiddenStackContains 方法还接受闭包作为它们的第二个参数,允许更精细地控制值比较操作。

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');

如果你想检索存储在上下文中的所有信息,可以调用 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::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'));
});
}
lightbulb

不要在 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'));
}
});
}
lightbulb

不要在 hydrated 回调中使用 Context 门面,而是确保只对传递给回调的存储库进行更改。