При работе с 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