Laravel 的“上下文”功能使你能够捕获、检索和共享贯穿你的应用程序中执行的请求、任务和命令的信息。这些捕获的信息也包含在你的应用程序写入的日志中,使你能够更深入地了解在日志条目写入之前发生的周围代码执行历史,并允许你追踪贯穿整个分布式系统的执行流。
了解 Laravel 上下文能力的最佳方式是利用其内置的日志功能并观察它的实际运作。要开始使用,你可以使用 Context facade 向上下文中添加信息。在此示例中,我们将使用一个 中间件 在每个传入请求上将请求 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"Context 还提供了方便的方法来递增或递减给定的键。这两种方法都至少接受一个参数:要跟踪的键。可以提供第二个参数来指定键应递增或递减的量:
Context::increment('records_added');
Context::increment('records_added', 5);
Context::decrement('records_added');
Context::decrement('records_added', 5);when 方法可用于根据给定条件向上下文中添加数据。提供给 when 方法的第一个闭包将在给定条件评估为 true 时被调用,而第二个闭包将在条件评估为 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', []),
);scope 方法提供了一种在给定回调执行期间临时修改上下文,并在回调执行完毕后将上下文恢复到其原始状态的方法。此外,你可以在闭包执行期间传递应合并到上下文中的额外数据(作为第二个和第三个参数)。
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Log;
Context::add('trace_id', 'abc-999');
Context::addHidden('user_id', 123);
Context::scope(
function () {
Context::add('action', 'adding_friend');
$userId = Context::getHidden('user_id');
Log::debug("Adding user [{$userId}] to friends list.");
// Adding user [987] to friends list. {"trace_id":"abc-999","user_name":"taylor_otwell","action":"adding_friend"}
},
data: ['user_name' => 'taylor_otwell'],
hidden: ['user_id' => 987],
);
Context::all();
// [
// 'trace_id' => 'abc-999',
// ]
Context::allHidden();
// [
// 'user_id' => 123,
// ][!WARNING]
如果上下文中的一个对象在作用域闭包内被修改,该修改将在作用域外反映出来。
Context 提供了创建“栈”的能力,栈是一种数据列表,其数据按添加顺序存储。您可以通过调用 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;
// In AppServiceProvider.php...
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 和 except 方法可用于获取上下文中的部分信息:
$data = Context::only(['first_key', 'second_key']);
$data = Context::except(['first_key']);pull 方法可用于从上下文中获取信息并立即将其从上下文中移除:
$value = Context::pull('key');如果上下文数据存储在 堆栈 中,你可以使用 pop 方法从堆栈中弹出项:
Context::push('breadcrumbs', 'first_value', 'second_value');
Context::pop('breadcrumbs');
// second_value
Context::get('breadcrumbs');
// ['first_value']remember 和 rememberHidden 方法可用于从上下文中获取信息,同时如果请求的信息不存在,则将上下文值设为给定闭包返回的值:
$permissions = Context::remember(
'user-permissions',
fn () => $user->permissions,
);如果您想检索上下文中存储的所有信息,您可以调用 all 方法:
$data = Context::all();您可以使用 has 和 missing 方法来确定该上下文是否为给定键存储了任何值:
use Illuminate\Support\Facades\Context;
if (Context::has('key')) {
// ...
}
if (Context::missing('key')) {
// ...
}has 方法将返回 true 无论存储的值是什么。因此,例如,一个具有 null 值的键仍将被视为存在:
Context::add('key', null);
Context::has('key');
// trueforget 方法可用于从当前上下文中移除键及其值:
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::exceptHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::missingHidden(/* ... */);
Context::forgetHidden(/* ... */);上下文发出两个事件,允许你介入该上下文的水合与脱水过程。
为了说明这些事件如何使用,设想在您的应用程序的一个中间件中您设置了 app.locale 配置值根据传入的 HTTP 请求的 Accept-Language 头。Context 的事件允许您在请求期间捕获此值并在队列上恢复它,确保在队列上发送的通知具有正确的 app.locale 值。我们可以使用 Context 的事件和 隐藏 数据来实现这一点,以下文档将对此进行说明。
每当作业被分派到队列时,上下文中的数据会被“脱水”并与作业的有效负载一起捕获。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'));
}
});
}[!NOTE]
你不应该在hydrated回调中使用Context门面,而是要确保你只对传递给回调的仓库进行更改。