跳至內容

情境

簡介

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

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

Context::push('breadcrumbs', 'first_value', 'second_value');
 
Context::pop('breadcrumbs')
// second_value
 
Context::get('breadcrumbs');
// ['first_value']

如果您想檢索儲存在情境中的所有資訊,您可以叫用 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::popHidden(/* ... */);
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 facade,因為這會改變目前程序的上下文。請確保你只對傳遞給回呼函式的儲存庫進行更改。

水合

每當一個佇列中的任務開始在佇列上執行時,任何與該任務共享的上下文都會被「復原」回當前的上下文中。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 facade,而是確保你只對傳遞給回呼函式的儲存庫進行更改。