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