Поддержите проект сделав пожертвование.
Ищете работу? Мы поможем!
Ищете работу? Мы поможем!

Контекст

Вы просматриваете документ для прошлой версии.
Рассмотрите возможность обновления вашего проекта до актуальной версии 12.x. Почему это важно?

Введение

«Контекстные» возможности 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
{
    /**
     * Обработка входящего запроса.
     */
    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('Пользователь прошел аутентификацию.', ['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 в очередь после добавления некоторой информации в контекст:

// В нашем посреднике...
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('Обработка подкаста.', [
            'podcast_id' => $this->podcast->id,
        ]);

        // ...
    }
}

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

Обработка подкаста. {"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;

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