跳到主要內容

上下文

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

上下文

簡介

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
{
    /**
     * 處理傳入的請求。
     */
    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 任務分派到佇列:

// 在我們的中介層中...
Context::add('url', $request->url());
Context::add('trace_id', Str::uuid()->toString());

// 在我們的控制器中...
ProcessPodcast::dispatch($podcast);

當任務被分派時,目前儲存在上下文中的任何資訊都會被擷取並與任務共享。然後,在任務執行期間,擷取的資訊會被序列化回當前上下文。因此,如果我們任務的 handle 方法要寫入日誌:

class ProcessPodcast implements ShouldQueue
{
    use Queueable;

    // ...

    /**
     * 執行任務。
     */
    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 方法可用於基於給定條件向上下文添加資料。如果給定條件評估為 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', []),
);

作用域上下文

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;

// 在 AppServiceProvider.php 中...
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');

onlyexcept 方法可用於檢索上下文中的部分資訊:

$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']

rememberrememberHidden 方法可用於從上下文中檢索資訊,同時在請求的資訊不存在時將上下文值設定為給定閉包返回的值:

$permissions = Context::remember(
    'user-permissions',
    fn () => $user->permissions,
);

如果您希望檢索儲存在上下文中的所有資訊,您可以呼叫 all 方法:

$data = Context::all();

判斷項目是否存在

您可以使用 hasmissing 方法判斷上下文是否為給定鍵儲存了任何值:

use Illuminate\Support\Facades\Context;

if (Context::has('key')) {
    // ...
}

if (Context::missing('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']);

隱藏上下文

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

事件

Context 派發兩個事件,允許您掛鉤到上下文的序列化和反序列化過程。

為了說明如何使用這些事件,想像在應用程式的一個中介層中,您根據傳入 HTTP 請求的 Accept-Language 標頭設定 app.locale 配置值。Context 的事件允許您在請求期間擷取此值並在佇列上恢復它,確保在佇列上發送的通知具有正確的 app.locale 值。我們可以使用上下文的事件和隱藏資料來實現這一點,以下文件將進行說明。

反序列化

每當任務被分派到佇列時,上下文中的資料都會被「反序列化」並與任務的有效載荷一起擷取。Context::dehydrating 方法允許您註冊一個閉包,該閉包將在反序列化過程中被呼叫。在此閉包內,您可以對將與佇列任務共享的資料進行更改。

通常,您應該在應用程式的 AppServiceProvider 類別的 boot 方法中註冊 dehydrating 回呼:

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Context::dehydrating(function (Repository $context) {
        $context->addHidden('locale', Config::get('app.locale'));
    });
}

[!NOTE] 您不應在 dehydrating 回呼內使用 Context 外觀,因為這會更改當前進程的上下文。確保您只對傳遞給回呼的儲存庫進行更改。

序列化

每當佇列任務開始在佇列上執行時,與任務共享的任何上下文都會被「序列化」回當前上下文。Context::hydrated 方法允許您註冊一個閉包,該閉包將在序列化過程中被呼叫。

通常,您應該在應用程式的 AppServiceProvider 類別的 boot 方法中註冊 hydrated 回呼:

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;

/**
 * 啟動任何應用程式服務。
 */
public function boot(): void
{
    Context::hydrated(function (Repository $context) {
        if ($context->hasHidden('locale')) {
            Config::set('app.locale', $context->getHidden('locale'));
        }
    });
}

[!NOTE] 您不應在 hydrated 回呼內使用 Context 外觀,而應確保您只對傳遞給回呼的儲存庫進行更改。