Контекст

Вступ

Можливості "контексту" Laravel дозволяють захоплювати, отримувати та ділитися інформацією протягом запитів, завдань і команд, що виконуються у вашому застосунку. Ця захоплена інформація також включається в журнали, записані вашим застосунком, надаючи вам глибше розуміння історії виконання коду, що відбулася перед записом журналу, і дозволяючи відстежувати потоки виконання в розподіленій системі.

Як це працює

Найкращий спосіб зрозуміти можливості контексту в Laravel — це побачити їх у дії, використовуючи вбудовані функції логування. Щоб почати, ви можете додати інформацію до контексту, використовуючи фасад Context. У цьому прикладі ми використаємо middleware, щоб додати URL запиту та унікальний ідентифікатор трасування до контексту для кожного вхідного запиту:

<?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, переданий до запису журналу, але також міститиме url та trace_id контексту як метадані:

Користувача автентифіковано. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

Інформація, додана до контексту, також стає доступною для завдань, відправлених у чергу. Наприклад, уявіть, що ми відправляємо завдання ProcessPodcast у чергу після додавання деякої інформації до контексту:

// У нашому middleware...
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,
]);
 
// ...
}
}

Результуючий запис у журналі міститиме інформацію, яка була додана до контексту під час запиту, що спочатку відправив завдання:

Обробка подкасту. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

Хоча ми зосередилися на вбудованих функціях логування в контексті Laravel, наступна документація покаже, як контекст дозволяє ділитися інформацією через межу HTTP-запиту / поставленої в чергу роботи і навіть як додати приховані дані контексту, які не записуються з записами журналу.

Захоплення Контексту

Ви можете зберігати інформацію в поточному контексті, використовуючи метод add фасаду Context:

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::increment('records_added');
Context::increment('records_added', 5);
 
Context::decrement('records_added');
Context::decrement('records_added', 5);

Умовний Контекст

Метод when може бути використаний для додавання даних до контексту на основі заданої умови. Перше замикання, надане методу when, буде викликано, якщо задана умова оцінюється як true, тоді як друге замикання буде викликано, якщо умова оцінюється як 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.");
// Додавання користувача [987] до списку друзів. {"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,
// ]

Якщо об'єкт у контексті змінюється всередині обмеженого замикання, ця зміна буде відображена за межами цього обсягу.

Стекі

Контекст надає можливість створювати "стеки", які є списками даних, збереженими в порядку їх додавання. Ви можете додати інформацію до стека, викликавши метод 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]);
});

Ви можете визначити, чи є значення в стеку, використовуючи методи stackContains та hiddenStackContains:

if (Context::stackContains('breadcrumbs', 'first_value')) {
    //
}
 
if (Context::hiddenStackContains('secrets', 'first_value')) {
    //
}

Методи stackContains та hiddenStackContains також приймають замикання як другий аргумент, що дозволяє більше контролювати операцію порівняння значень:

use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
 
return Context::stackContains('breadcrumbs', function ($value) {
    return Str::startsWith($value, 'query_');
});

Отримання Контексту

Ви можете отримати інформацію з контексту, використовуючи метод get фасаду Context:

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

Методи only та except можуть бути використані для отримання підмножини інформації в контексті:

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

Якщо ви хочете отримати всю інформацію, що зберігається в контексті, ви можете викликати метод all:

$data = Context::all();

Визначення Існування Елемента

Ви можете використовувати методи has та missing, щоб визначити, чи є в контексті значення, збережене для заданого ключа:

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

Прихований контекст

Контекст надає можливість зберігати "приховані" дані. Ця прихована інформація не додається до журналів і не доступна через методи отримання даних, задокументовані вище. Контекст надає інший набір методів для взаємодії з прихованою контекстною інформацією:

use Illuminate\Support\Facades\Context;
 
Context::addHidden('key', 'value');
 
Context::getHidden('key');
// 'value'
 
Context::get('key');
// null

Методи "hidden" відображають функціональність не прихованих методів, задокументованих вище:

Context::addHidden(/* ... */);
Context::addHiddenIf(/* ... */);
Context::pushHidden(/* ... */);
Context::getHidden(/* ... */);
Context::pullHidden(/* ... */);
Context::popHidden(/* ... */);
Context::onlyHidden(/* ... */);
Context::exceptHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::missingHidden(/* ... */);
Context::forgetHidden(/* ... */);

Події

Контекст викликає дві події, які дозволяють вам підключитися до процесу гідратації та дегідратації контексту.

Щоб проілюструвати, як ці події можуть бути використані, уявіть, що в middleware вашого застосунку ви встановлюєте значення конфігурації app.locale на основі заголовка Accept-Language вхідного HTTP-запиту. Події контексту дозволяють захопити це значення під час запиту та відновити його в черзі, забезпечуючи, що сповіщення, надіслані в черзі, мають правильне значення app.locale. Ми можемо використовувати події контексту та приховані дані для досягнення цього, що буде проілюстровано в наступній документації.

Зневоднення

Коли завдання відправляється в чергу, дані в контексті "зневоднюються" і захоплюються разом із вмістом завдання. Метод Context::dehydrating дозволяє зареєструвати замикання, яке буде викликано під час процесу зневоднення. У цьому замиканні ви можете вносити зміни до даних, які будуть передані в чергу завдання.

Зазвичай, ви повинні реєструвати зворотні виклики dehydrating в методі boot класу AppServiceProvider вашого застосунку:

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

Ви не повинні використовувати фасад Context всередині зворотного виклику dehydrating, оскільки це змінить контекст поточного процесу. Переконайтеся, що ви вносите зміни лише до репозиторію, переданого у зворотний виклик.

Гідратований

Коли запланована задача починає виконуватися в черзі, будь-який контекст, що був спільним із задачею, буде "гідратований" назад у поточний контекст. Метод Context::hydrated дозволяє зареєструвати замикання, яке буде викликано під час процесу гідратації.

Зазвичай, ви повинні реєструвати зворотні виклики hydrated в методі boot класу AppServiceProvider вашого застосунку:

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

Ви не повинні використовувати фасад Context всередині зворотного виклику hydrated, а натомість переконайтеся, що ви вносите зміни лише до репозиторію, переданого у зворотний виклик.