Працюючи з Laravel, ми зазвичай передаємо дані явно:
$user = auth()->user();
UserExampleService::process($user);
Або додаємо інформацію безпосередньо до запису логу:
Log::info('Processing order', [
'order_id' => $order->id,
]);
А що робити, коли деяка інформація має бути доступна протягом усього поточного виконання?
Наприклад:
- trace ID;
- поточний URL;
- ідентифікатор орендаря (tenant ID);
- назва операції;
- інформація, потрібна для логування;
- інформація, яка має переходити в завдання черги.
Саме тут стає цікавою можливість Laravel під назвою Context.
Що таке Laravel Context?
Laravel Context - це місце для інформації, яка належить до поточного виконання вашого застосунку.
Ви додаєте дані:
use Illuminate\Support\Facades\Context;
Context::add('trace_id', 'abc-123');
І отримуєте їх в іншому місці:
$traceId = Context::get('trace_id');
Найцікавіше те, що вам не доводиться передавати $traceId через кожен метод.
Інформація живе в поточному контексті, і нею можуть користуватися інші частини застосунку.
Сам Laravel описує Context як спосіб збирати, отримувати й поширювати інформацію в межах запитів, завдань і команд. До того ж ця інформація інтегрована з логами застосунку.
Найпростіший приклад
Уявімо middleware, що генерує trace 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"
}
У документації Laravel 13 дані контексту не зливаються з даними запису в один масив, а додаються окремим блоком метаданих, щоб їх можна було відрізнити: User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-..."}.
Це зручно: ви задаєте інформацію один раз, і вона супроводжує всі наступні записи логу, замість того щоб додавати її вручну скрізь.
А ось тут Context стає по-справжньому цікавим
Уявімо, що відбувається таке:
HTTP-запит
│
├── Контролер
│ │
│ └── Сервіс
│ │
│ ├── контекст
│ └── Dispatch завдання
│
▼
Дані завдання (контекст зібрано, дегідратовано й збережено в даних)
│
▼
Черга
│
▼
Воркер
│
▼ гідратація -> відновлення контексту
│
▼
Виконання завдання
Зазвичай 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 - це довготривалий процес: він обробляє одне завдання, потім інше:
Воркер
│
├── Завдання A
│
├── Завдання B
│
├── Завдання C
│
└── Завдання D
Якби Context просто зберігався глобально й ніколи не очищувався, могло б вийти таке:
Завдання A
trace_id = abc-123
Завдання B
trace_id = abc-123 (а це погано)
Тому Laravel керує життєвим циклом навколо воркера (registerWorker()). Логіка скидання воркера в сервіс-провайдері черги серед інших операцій очищення скидає спільний контекст логування й очищає scoped-екземпляри між завданнями.
Контекст може бути прихованим
Не все, що ви кладете в Context, має з'являтися в логах. Саме для цього Laravel має прихований контекст:
Context::addHidden('internal_hidden_id', 123);
Context::getHidden('internal_hidden_id');
// 123
Але:
Context::get('internal_hidden_id');
// null
Прихований контекст не додається до записів логу й має окремий API.
Отже, маємо два види контексту:
Context
├── Звичайні дані
│ └── Можуть потрапляти в логи
│
└── Приховані дані
└── Доступні застосунку, але не потрапляють у логи
Це розрізнення особливо корисне, коли контекст містить внутрішню інформацію, якій не місце в логах застосунку.
Сам Laravel теж користується прихованим контекстом.
У CallQueuedHandler Laravel може зберігати таку інформацію:
laravel_unique_job_cache_store
laravel_unique_job_key
laravel_unique_job_lock_owner
Навіщо? Буває, що Laravel не може десеріалізувати завдання з черги, бо модель, на яку воно посилається, відсутня.
У такому разі Laravel однаково потрібно достатньо інформації, щоб зняти блокування унікального завдання.
Обробник бере ці значення з прихованого контексту й використовує їх, щоб відновити або зняти блокування.
У контексту може бути життєвий цикл
Laravel також дає змогу втрутитися в момент, коли контекст перетинає межу черги.
Зазвичай колбеки dehydrating і hydrated реєструють у методі boot класу AppServiceProvider вашого застосунку.
Наприклад:
Context::dehydrating(function (Repository $context) {
// Підготувати контекст, перш ніж його буде прикріплено до завдання
$context->addHidden('locale', Config::get('app.locale'));
});
І:
Context::hydrated(function (Repository $context) {
// Відновити або використати контекст, коли завдання стартує
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 особливо доречний, коли інформація належить до потоку виконання, а не до конкретної функції.
Наприклад:
Запит
│
├── Middleware
│
├── Контролер
│
├── Сервіс
│
├── Подія
│
├── Завдання черги
│
└── Логи
Значення на кшталт:
trace_id = abc-123
описує всю операцію, а не один конкретний метод. Саме тут Context і розкривається: замість того щоб скрізь писати
processOrder($order, $traceId);
можна тримати інформацію для трасування в Context:
Context::get('trace_id');
і дозволити фреймворку переносити її крізь увесь потік виконання.
Одне важливе правило
Context не замінює звичайних даних застосунку, тож не кладіть у нього все підряд. Корисна ментальна модель:
Аргументи описують, що потрібно функції. Контекст описує, що відбувається навколо поточного виконання.
Наприклад:
processOrder($order);
має сенс, бо $order - це вхідні дані операції.
Але й:
Context::add('trace_id', $traceId);
має сенс, бо trace ID описує виконання, в межах якого відбувається ця операція.
То зрештою це ще одне сховище ключ-значення?
Сам API простий:
Context::add(...)
Context::get(...)
Цікаве те, що Laravel робить із цією інформацією.
Він може:
- додавати її до логів;
- переносити її з HTTP-запиту в завдання черги;
- відновлювати її, коли завдання стартує;
- тримати приховані дані окремо від даних для логів;
- давати застосунку хуки навколо цього перенесення.
Тож наступного разу, коли побачите:
Context::add('trace_id', $traceId);
не думайте про це як про ще одне сховище ключ-значення в Laravel. Думайте про це як про метадані, прикріплені до потоку виконання - саме це й робить Laravel Context набагато цікавішим, ніж здається на перший погляд.
Джерело: документація Laravel про Context.