跳轉至內容

上下文 (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 門面,並確保僅對傳遞給回撥的儲存庫進行更改。