Поддержите проект сделав пожертвование.
Примите наш вызов и улучшите свои навыки!
Примите наш вызов и улучшите свои навыки!

Введение

«Контекстные» возможности Laravel позволяют вам собирать, извлекать и обмениваться информацией в запросах, заданиях и командах, выполняющихся в вашем приложении. Эта собранная информация также включается в журналы, записываемые вашим приложением, что дает вам более глубокое представление об истории выполнения окружающего кода, произошедшей до того, как была записана запись в журнале, и позволяет отслеживать потоки выполнения во всей распределенной системе.

Как это работает

Лучший способ понять контекстные возможности Laravel — увидеть его в действии, используя встроенные функции ведения журнала. Для начала вы можете добавить информацию в контекст, используя фасад Context. В этом примере мы будем использовать посредника для добавления 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"}

Хотя мы сосредоточились на встроенных функциях контекста Laravel, связанных с ведением журнала, следующая документация покажет, как контекст позволяет вам обмениваться информацией через границу HTTP-запроса/задания в очереди и даже как добавлять [данные скрытого контекста](#hidden- контекст), который не записывается в записи журнала.

Захват контекста

Вы можете хранить информацию в текущем контексте, используя метод 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"

Условный контекст

Метод 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', []),
);

Стеки

Контекст предлагает возможность создавать «стеки», которые представляют собой списки данных, хранящихся в том порядке, в котором они были добавлены. Вы можете добавить информацию в стек, вызвав метод 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]);
});

Вы можете определить, находится ли значение в стеке, используя методы 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 можно использовать для получения подмножества информации в контексте:

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

События

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

Чтобы проиллюстрировать, как можно использовать эти события, представьте, что в промежуточном программном обеспечении вашего приложения вы устанавливаете значение конфигурации app.locale на основе заголовка Accept-Language входящего HTTP-запроса. События контекста позволяют вам захватить это значение во время запроса и восстановить его в очереди, гарантируя, что отправляемые в очередь уведомления имеют правильное значение app.locale. Для достижения этой цели мы можем использовать события контекста и данные hidden, что будет показано в следующей документации.

Обезвоживание

Всякий раз, когда задание отправляется в очередь, данные в контексте «обезвоживаются» и фиксируются вместе с полезной нагрузкой задания. Метод 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 и вместо этого убедитесь, что вы вносите изменения только в репозиторий, переданный в обратный вызов.