Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти
Інше
Події Про нас

Контекст

Вступ

Можливості «контексту» в 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
{
    /**
     * 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, переданий запису, а також url і trace_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"}

Хоча ми зосередилися на вбудованих можливостях логування, наведена далі документація покаже, як контекст дозволяє передавати інформацію через межу «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.");
        // 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,
// ]

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

Стеки

Контекст дозволяє створювати «стеки» - списки даних, збережених у порядку додавання. Додати інформацію до стека можна методом 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;

// In 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']

Методи remember і rememberHidden дозволяють отримати інформацію з контексту, водночас задавши значення контексту тим, що повертає передане замикання, якщо запитаної інформації немає:

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

Якщо ви хочете отримати всю збережену в контексті інформацію, викличте метод 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

«Приховані» методи дзеркалять функціональність звичайних методів, описаних вище:

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;

/**
 * Bootstrap any application services.
 */
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;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Context::hydrated(function (Repository $context) {
        if ($context->hasHidden('locale')) {
            Config::set('app.locale', $context->getHidden('locale'));
        }
    });
}

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