При работе с Laravel мы обычно передаем данные явно:
$user = auth()->user();
UserExampleService::process($user);
Или добавляем информацию напрямую в лог:
Log::info('Processing order', [
'order_id' => $order->id,
]);
Но что, если у вас есть информация, которая должна быть доступна на протяжении всего текущего жизненного цикла выполнения?
Например:
- ID трассировки (trace ID)
- текущий URL
- ID тенанта (tenant ID)
- название операции
- информация, необходимая для логирования
- информация, которая должна передаваться вместе с задачей в очереди
Именно здесь становится интересной функция Context в Laravel.
Что такое Laravel Context?
Laravel Context предоставляет место для хранения информации, относящейся к текущему выполнению вашего приложения.
Вы можете добавить данные:
use Illuminate\Support\Facades\Context;
Context::add('trace_id', 'abc-123');
И получить их в другом месте:
$traceId = Context::get('trace_id');
Самое интересное, что вам не нужно постоянно передавать $traceId через каждый метод.
Информация живет в текущем контексте и может быть использована другими частями приложения.
Laravel описывает Context как способ фиксации, извлечения и совместного использования информации в запросах, задачах и командах. Он также интегрирует эту информацию с логами приложения.
Простейший пример
Представьте себе middleware, генерирующий ID трассировки для каждого запроса:
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
Context::add('trace_id', Str::uuid()->toString());
Теперь где-то глубоко внутри вашего приложения:
$traceId = Context::get('trace_id');
- Никаких параметров в контроллере.
- Никаких параметров в сервисе.
- Никаких глобальных переменных.
Данные просто являются частью текущего контекста.
Действительно полезная часть: логирование
Context становится особенно полезным в сочетании с логированием.
Например:
Context::add([
'trace_id' => Str::uuid()->toString(),
'url' => $request->url(),
]);
Затем позже:
Log::info('User authenticated', [
'user_id' => $user->id,
]);
Запись в логе может содержать как информацию, явно переданную в лог, так и информацию, сохраненную в Context:
User authenticated
{
"user_id": 27,
"trace_id": "e04e1a11-...",
"url": "https://dev.to/login"
}
Это удобно, потому что вы можете задать информацию один раз, и она будет сопровождать последующие записи в логе, вместо того чтобы вручную добавлять ее повсюду.
Но вот где Context становится по-настоящему интересным
Представьте, что происходит следующее:
HTTP Request
│
|--- Controller
│ │
│ |--- Service
│ │
| |--- context
│ |--- Dispatch Job
│
v
Job Payload (Context captured, dehydrated, and stored within payload)
│
v
Queu
│
v
Worker
│
v hydarte -> restore Context
│
v
Job Execution
Обычно HTTP-запрос и задача в очереди — это два разных процесса выполнения.
Как же задача узнает о контексте, созданном во время исходного запроса?
Laravel берет это на себя: при отправке задачи Laravel фиксирует текущий контекст и сохраняет его вместе с полезной нагрузкой задачи.
Когда воркер начинает обрабатывать задачу, этот контекст восстанавливается в текущем процессе выполнения. Laravel называет эти два этапа dehydrating (дегидратация) и hydrating (гидратация).
Таким образом, это:
Context::add('trace_id', 'abc-123');
ProcessPodcast::dispatch($podcast);
может привести к тому, что задача увидит:
Context::get('trace_id');
// "abc-123"
И следовательно:
Log::info('Processing podcast');
все еще может содержать исходную информацию о трассировке. Это удивительно мощная функция для отладки распределенных рабочих процессов.
Таким образом, Context — это не глобальная переменная, разделяемая между процессами.
Вот еще одна интересная деталь
Очередь воркера Laravel — это долгоживущий процесс, он обрабатывает одну задачу, затем другую:
Worker
│
|--- Job A
│
|--- Job B
│
|--- Job C
│
|--- Job D
Если бы Context просто хранился глобально и никогда не очищался, вы могли бы получить:
Job A
trace_id = abc-123
Job B
trace_id = abc-123 (this is bad)
Поэтому в Laravel реализовано управление жизненным циклом воркера (через метод registerWorker()). Логика сброса состояния воркера в сервис-провайдере очередей очищает общий контекст логирования и освобождает изолированные инстансы между задачами, помимо других операций по очистке.
Контекст также может быть скрытым
Не все, что вы помещаете в Context, должно появляться в ваших логах. Laravel предоставляет скрытый контекст (hidden context) именно для этой цели:
Context::addHidden('internal_hidden_id', 123);
Context::getHidden('internal_hidden_id');
// 123
Но:
Context::get('internal_hidden_id');
// null
Скрытый контекст не добавляется к записям логов и использует отдельный API.
Это дает вам два разных типа контекста:
Context
|-- Normal data
│ |--- Can be included in logs
│
|-- Hidden data
|--- Available to your application, but not included in logs.
Это различие становится особенно полезным, когда контекст содержит внутреннюю информацию, которая не должна попадать в логи вашего приложения.
Laravel сам использует скрытый Context внутри себя.
В классе CallQueuedHandler Laravel может хранить такую информацию, как:
laravel_unique_job_cache_store
laravel_unique_job_key
laravel_unique_job_lock_owner
Почему? Возможна ситуация, когда Laravel не может десериализовать задачу из очереди из-за отсутствия модели.
В этом случае Laravel все равно требуется достаточно информации, чтобы снять блокировку уникальной задачи (unique-job lock).
Обработчик извлекает эти значения из скрытого Context и использует их для восстановления/снятия блокировки.
У контекста может быть жизненный цикл
Laravel также позволяет вам подключаться к моменту, когда контекст пересекает границу очереди.
Обычно вам следует регистрировать коллбеки dehydrating / hydrated внутри метода boot класса AppServiceProvider вашего приложения:
Например:
Context::dehydrating(function (Repository $context) {
// Prepare context before it is attached to a job
$context->addHidden('locale', Config::get('app.locale'));
});
И:
Context::hydrated(function (Repository $context) {
// Restore or use context when the job starts
if ($context->hasHidden('locale')) {
Config::set('app.locale', $context->getHidden('locale'));
}
});
Это означает, что Context — это не просто статический массив, лежащий где-то в памяти.
Laravel предоставляет вам хуки для отслеживания перехода из одной среды выполнения в другую.
Важная деталь из документации Laravel: внутри этих коллбеков вам следует изменять репозиторий $context , переданный в коллбек, а не вызывать сам фасад Context .
Context — это больше, чем хранилище ключ/значение
Существуют полезные функции помимо add() и get() .
Например, вы можете хранить стек:
Context::push('breadcrumbs', 'checkout');
Context::push('breadcrumbs', 'payment');
Context::get('breadcrumbs');
Вы также можете получить значение и одновременно удалить его:
$value = Context::pull('key');
Или запомнить значение только в том случае, если оно еще не существует:
$permissions = Context::remember(
'user-permissions',
fn () => $user->permissions,
);
Laravel также предоставляет методы has , missing , forget , only , except и соответствующие методы для скрытого контекста.
Вам не понадобятся все они каждый день. Главное — понимать, для чего предназначен Context.
Когда его следует использовать?
Context особенно интересен, когда информация относится к потоку выполнения, а не к конкретной функции.
Например:
Request
│
|--- Middleware
│
|--- Controller
│
|--- Service
│
|--- Event
│
|--- Queue Job
│
|-- Logs
Такое значение, как:
trace_id = abc-123
может описывать всю операцию, а не один конкретный метод. Именно здесь Context раскрывает себя с лучшей стороны, поэтому вместо того, чтобы делать это повсюду:
processOrder($order, $traceId);
вы можете хранить информацию о трассировке в Context:
Context::get('trace_id');
и позволить фреймворку переносить ее по всему потоку выполнения.
Одно важное правило
Context не является заменой обычным данным приложения, не нужно помещать в него всё подряд. Полезная ментальная модель выглядит так:
Аргументы описывают то, что нужно функции. Контекст описывает то, что происходит вокруг текущего выполнения.
Например:
processOrder($order);
это имеет смысл, поскольку $order является входным параметром для операции.
Но:
Context::add('trace_id', $traceId);
это имеет смысл, поскольку ID трассировки описывает выполнение, окружающее эту операцию.
В итоге, это просто еще одно хранилище ключ/значение в Laravel?
Сам API прост:
Context::add(...)
Context::get(...)
Интересно то, что Laravel делает с этой информацией.
Он может:
- прикреплять ее к логам,
- переносить ее из HTTP-запроса в задачу в очереди,
- восстанавливать ее при запуске задачи,
- хранить скрытые данные отдельно от данных логов,
- и предоставлять вашему приложению хуки для управления этим переносом.
Поэтому в следующий раз, когда вы увидите:
Context::add('trace_id', $traceId);
не думайте об этом как о просто еще одном хранилище ключ/значение в Laravel, думайте об этом как о метаданных, привязанных к потоку выполнения, и именно это делает Laravel Context гораздо более интересным, чем кажется на первый взгляд.
Источники:
Документация по Context
Комментарии (0)
Пока нет комментариев — будьте первым.