介紹 (Introduction)
Laravel 的「context」功能可讓您在應用程式中執行的 requests、jobs 和 commands 之間擷取、取回與共享資訊。這些擷取的資訊也會包含在應用程式寫入的 logs 中,讓您更深入了解在寫入 log 條目之前發生的周圍程式碼執行歷史,並允許您追蹤整個分散式系統的執行流程。
運作方式 (How It Works)
了解 Laravel context 功能的最佳方式是使用內建的 logging 功能來實際操作。首先,您可以使用 Context facade 來將資訊加入到 context。在此範例中,我們將使用 middleware 在每個傳入 request 上將 request URL 和唯一的 trace ID 加入到 context:
<?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 的資訊會自動作為 metadata 附加到整個 request 過程中寫入的任何 log 條目。將 context 作為 metadata 附加可以區分傳遞給個別 log 條目的資訊與透過 Context 共享的資訊。例如,假設我們寫入以下 log 條目:
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
寫入的 log 將包含傳遞給 log 條目的 auth_id,但它也會包含 context 的 url 和 trace_id 作為 metadata:
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
加入到 context 的資訊也可供發送到 queue 的 jobs 使用。例如,假設我們在將一些資訊加入到 context 後,將 ProcessPodcast job 發送到 queue:
// In our middleware...
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());
// In our controller...
ProcessPodcast::dispatch($podcast);
當 job 被發送時,目前儲存在 context 中的任何資訊都會被擷取並與 job 共享。擷取的資訊會在 job 執行時被 hydrated 回到目前的 context 中。因此,如果我們的 job 的 handle 方法要寫入 log:
class ProcessPodcast implements ShouldQueue
{
use Queueable;
// ...
/**
* Execute the job.
*/
public function handle(): void
{
Log::info('Processing podcast.', [
'podcast_id' => $this->podcast->id,
]);
// ...
}
}
產生的 log 條目將包含在最初發送 job 的 request 期間加入到 context 的資訊:
Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
雖然我們專注於 Laravel context 的內建 logging 相關功能,但以下文件將說明 context 如何讓您跨 HTTP request / queued job 邊界共享資訊,甚至如何加入不會與 log 條目一起寫入的隱藏 context 資料。
擷取 Context (Capturing Context)
您可以使用 Context facade 的 add 方法將資訊儲存在目前的 context 中:
use Illuminate\Support\Facades\Context;
Context::add('key', 'value');
要一次加入多個項目,您可以傳遞一個關聯陣列給 add 方法:
Context::add([
'first_key' => 'value',
'second_key' => 'value',
]);
add 方法會覆蓋任何共享相同 key 的現有值。如果您只想在 key 尚不存在時才將資訊加入到 context,您可以使用 addIf 方法:
Context::add('key', 'first');
Context::get('key');
// "first"
Context::addIf('key', 'second');
Context::get('key');
// "first"
Context 還提供了方便的方法來遞增或遞減給定的 key。這兩個方法至少接受一個引數:要追蹤的 key。可以提供第二個引數來指定 key 應該遞增或遞減的量:
Context::increment('records_added');
Context::increment('records_added', 5);
Context::decrement('records_added');
Context::decrement('records_added', 5);
條件式 Context (Conditional Context)
when 方法可用於根據給定條件將資料加入到 context。如果給定條件評估為 true,則會呼叫提供給 when 方法的第一個 closure,如果條件評估為 false,則會呼叫第二個 closure:
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', []),
);
作用域 Context (Scoped Context)
scope 方法提供了一種在給定 callback 執行期間暫時修改 context,並在 callback 完成執行時將 context 還原到原始狀態的方式。此外,您可以傳遞額外的資料(作為第二和第三個引數),這些資料應在 closure 執行時合併到 context 中。
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] 如果在作用域 closure 內修改了 context 中的物件,該變更將反映在作用域外。
Stacks
Context 提供了建立「stacks」的功能,這是按照加入順序儲存的資料列表。您可以透過呼叫 push 方法將資訊加入到 stack:
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',
// ]
Stacks 可用於擷取有關 request 的歷史資訊,例如整個應用程式中發生的 events。例如,您可以建立一個 event listener,在每次執行查詢時推送到 stack,將查詢 SQL 和持續時間擷取為 tuple:
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 方法來判斷值是否在 stack 中:
if (Context::stackContains('breadcrumbs', 'first_value')) {
//
}
if (Context::hiddenStackContains('secrets', 'first_value')) {
//
}
stackContains 和 hiddenStackContains 方法也接受 closure 作為其第二個引數,允許更多控制值比較操作:
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
return Context::stackContains('breadcrumbs', function ($value) {
return Str::startsWith($value, 'query_');
});
取回 Context (Retrieving Context)
您可以使用 Context facade 的 get 方法從 context 取回資訊:
use Illuminate\Support\Facades\Context;
$value = Context::get('key');
only 和 except 方法可用於取回 context 中資訊的子集:
$data = Context::only(['first_key', 'second_key']);
$data = Context::except(['first_key']);
pull 方法可用於從 context 取回資訊並立即將其從 context 中移除:
$value = Context::pull('key');
如果 context 資料儲存在 stack 中,您可以使用 pop 方法從 stack 彈出項目:
Context::push('breadcrumbs', 'first_value', 'second_value');
Context::pop('breadcrumbs');
// second_value
Context::get('breadcrumbs');
// ['first_value']
remember 和 rememberHidden 方法可用於從 context 取回資訊,如果請求的資訊不存在,則將 context 值設定為給定 closure 傳回的值:
$permissions = Context::remember(
'user-permissions',
fn () => $user->permissions,
);
如果您想取回儲存在 context 中的所有資訊,您可以呼叫 all 方法:
$data = Context::all();
判斷項目是否存在 (Determining Item Existence)
您可以使用 has 和 missing 方法來判斷 context 是否為給定 key 儲存了任何值:
use Illuminate\Support\Facades\Context;
if (Context::has('key')) {
// ...
}
if (Context::missing('key')) {
// ...
}
無論儲存的值是什麼,has 方法都會傳回 true。因此,例如,具有 null 值的 key 將被視為存在:
Context::add('key', null);
Context::has('key');
// true
移除 Context (Removing Context)
forget 方法可用於從目前的 context 中移除 key 及其值:
use Illuminate\Support\Facades\Context;
Context::add(['first_key' => 1, 'second_key' => 2]);
Context::forget('first_key');
Context::all();
// ['second_key' => 2]
您可以透過向 forget 方法提供陣列來一次忘記多個 key:
Context::forget(['first_key', 'second_key']);
隱藏的 Context (Hidden Context)
Context 提供了儲存「隱藏」資料的功能。這些隱藏資訊不會附加到 logs,也無法透過上述記載的資料取回方法存取。Context 提供了一組不同的方法來與隱藏的 context 資訊互動:
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(/* ... */);
Events
Context 會發送兩個 events,讓您可以掛鉤到 context 的 hydration 和 dehydration 過程。
為了說明如何使用這些 events,假設在應用程式的 middleware 中,您根據傳入 HTTP request 的 Accept-Language header 設定 app.locale 設定值。Context 的 events 讓您可以在 request 期間擷取此值並在 queue 上還原它,確保在 queue 上發送的 notifications 具有正確的 app.locale 值。我們可以使用 context 的 events 和隱藏資料來實現這一點,以下文件將說明這一點。
Dehydrating
每當 job 被發送到 queue 時,context 中的資料會被「dehydrated」並與 job 的 payload 一起擷取。Context::dehydrating 方法讓您可以註冊一個將在 dehydration 過程中呼叫的 closure。在此 closure 中,您可以對將與 queued job 共享的資料進行變更。
通常,您應該在應用程式的 AppServiceProvider 類別的 boot 方法中註冊 dehydrating callbacks:
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'));
});
}
[!NOTE] 您不應該在
dehydratingcallback 中使用Contextfacade,因為這將改變目前 process 的 context。請確保您只對傳遞給 callback 的 repository 進行變更。
Hydrated
每當 queued job 開始在 queue 上執行時,與 job 共享的任何 context 都將被「hydrated」回到目前的 context 中。Context::hydrated 方法讓您可以註冊一個將在 hydration 過程中呼叫的 closure。
通常,您應該在應用程式的 AppServiceProvider 類別的 boot 方法中註冊 hydrated callbacks:
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] 您不應該在
hydratedcallback 中使用Contextfacade,而是確保您只對傳遞給 callback 的 repository 進行變更。