跳到內容

Context

簡介

Laravel 的「context」功能使您能夠在應用程式中執行的請求、任務和命令中捕捉、檢索和共享資訊。此捕捉到的資訊也包含在應用程式寫入的日誌中,讓您更深入地了解日誌條目寫入之前發生的周圍程式碼執行歷史記錄,並讓您追蹤整個分散式系統中的執行流程。

運作方式

理解 Laravel context 功能的最佳方法是透過使用內建的日誌記錄功能來查看它的實際運作。要開始使用,您可以將資訊新增到 context,使用 Context facade。在本範例中,我們將使用中介層,在每個傳入的請求中將請求 URL 和唯一的追蹤 ID 新增到 context 中

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 的資訊會自動附加為元數據到在整個請求中寫入的任何日誌條目。將 context 作為元數據附加,可以區分傳遞給個別日誌條目的資訊和透過 Context 共享的資訊。例如,假設我們寫入以下日誌條目

1Log::info('User authenticated.', ['auth_id' => Auth::id()]);

寫入的日誌將包含傳遞給日誌條目的 auth_id,但它也將包含 context 的 urltrace_id 作為元數據

1User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

新增到 context 的資訊也可供調度到佇列的任務使用。例如,假設我們在將一些資訊新增到 context 後,將 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);

當任務被調度時,目前儲存在 context 中的任何資訊都會被捕捉並與任務共享。然後,捕捉到的資訊在任務執行時被水合回目前的 context 中。因此,如果我們的任務的 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}

產生的日誌條目將包含在最初調度任務的請求期間新增到 context 的資訊

1Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

雖然我們專注於 Laravel context 的內建日誌記錄相關功能,但以下文件將說明 context 如何讓您跨 HTTP 請求/佇列任務邊界共享資訊,甚至如何新增隱藏 context 資料,這些資料不會與日誌條目一起寫入。

捕捉 Context

您可以使用 Context facade 的 add 方法將資訊儲存在目前的 context 中

1use Illuminate\Support\Facades\Context;
2 
3Context::add('key', 'value');

要一次新增多個項目,您可以將關聯陣列傳遞給 add 方法

1Context::add([
2 'first_key' => 'value',
3 'second_key' => 'value',
4]);

add 方法將覆蓋任何共享相同鍵的現有值。如果您只想在鍵尚不存在時將資訊新增到 context,您可以使用 addIf 方法

1Context::add('key', 'first');
2 
3Context::get('key');
4// "first"
5 
6Context::addIf('key', 'second');
7 
8Context::get('key');
9// "first"

條件 Context

when 方法可用於根據給定的條件將資料新增到 context。如果給定的條件評估為 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);

堆疊

Context 提供創建「堆疊」的能力,這些堆疊是以新增順序儲存的資料列表。您可以透過調用 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});

您可以使用 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

您可以使用 Context facade 的 get 方法從 context 檢索資訊

1use Illuminate\Support\Facades\Context;
2 
3$value = Context::get('key');

only 方法可用於檢索 context 中資訊的子集

1$data = Context::only(['first_key', 'second_key']);

pull 方法可用於從 context 檢索資訊,並立即從 context 中移除它

1$value = Context::pull('key');

如果 context 資料儲存在堆疊中,您可以使用 pop 方法從堆疊中彈出項目

1Context::push('breadcrumbs', 'first_value', 'second_value');
2 
3Context::pop('breadcrumbs')
4// second_value
5 
6Context::get('breadcrumbs');
7// ['first_value']

如果您想檢索儲存在 context 中的所有資訊,您可以調用 all 方法

1$data = Context::all();

判斷項目是否存在

您可以使用 hasmissing 方法判斷 context 是否有為給定鍵儲存任何值

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

移除 Context

forget 方法可用於從目前的 context 中移除鍵及其值

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

隱藏 Context

Context 提供儲存「隱藏」資料的能力。此隱藏資訊不會附加到日誌,並且無法透過上面記錄的資料檢索方法存取。Context 提供一組不同的方法來與隱藏 context 資訊互動

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(/* ... */);

事件

Context 分派兩個事件,讓您可以掛鉤到 context 的水合和脫水過程。

為了說明如何使用這些事件,假設在應用程式的中介層中,您根據傳入的 HTTP 請求的 Accept-Language 標頭設定 app.locale 設定值。Context 的事件讓您可以在請求期間捕捉此值,並在佇列中還原它,確保在佇列上傳送的通知具有正確的 app.locale 值。我們可以使用 context 的事件和隱藏資料來達成此目的,以下文件將說明這一點。

脫水

每當任務被調度到佇列時,context 中的資料都會被「脫水」並與任務的有效負載一起捕捉。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。確保您只對傳遞給回調的儲存庫進行變更。

水合

每當佇列任務開始在佇列上執行時,與任務共享的任何 context 都會被「水合」回目前的 context 中。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,而是確保您只對傳遞給回調的儲存庫進行變更。