> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Context（上下文）

> 介绍如何使用 Laravel Context 功能在请求、任务、命令之间共享信息，并自动附加到日志中。

## 什么是 Context

Laravel 的 Context 功能提供了一种在请求、队列任务、命令之间跨越执行边界记录与共享信息的机制。
通过 `Illuminate\Support\Facades\Context` 门面添加的信息，会自动附带到应用写出的所有日志条目中。

这样就能明确区分某次日志调用附带的信息与 Context 保存的共享信息。
在使用队列或分布式架构进行追踪时尤其有用。

### 上下文的传播流程

```mermaid theme={null}
sequenceDiagram
    participant Client
    participant Middleware
    participant Controller
    participant Queue
    participant Job

    Client->>Middleware: HTTP 请求
    Middleware->>Middleware: Context::add('trace_id', uuid)
    Middleware->>Controller: next($request)
    Controller->>Controller: Log::info(...) — 自动附加 trace_id
    Controller->>Queue: ProcessPodcast::dispatch()
    Queue->>Job: 将上下文序列化后发送
    Job->>Job: 恢复上下文（Hydrate）
    Job->>Job: Log::info(...) — 自动附加 trace_id
```

## 基本用法

最典型的用法是在中间件中设置 `trace_id`。之后所有的日志都会自动包含它。

<Steps>
  <Step title="创建中间件">
    ```shell theme={null}
    php artisan make:middleware AddContext
    ```
  </Step>

  <Step title="向 Context 添加追踪 ID">
    ```php theme={null}
    <?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);
        }
    }
    ```
  </Step>

  <Step title="注册中间件">
    在 `bootstrap/app.php` 中作为全局中间件注册。

    ```php theme={null}
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(\App\Http\Middleware\AddContext::class);
    })
    ```
  </Step>
</Steps>

配置好后，控制器或服务写入的日志会自动附带 `url` 和 `trace_id`。

```php theme={null}
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
```

```text theme={null}
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
```

## 写入上下文

### add — 添加值

```php theme={null}
use Illuminate\Support\Facades\Context;

Context::add('key', 'value');

// 一次添加多个
Context::add([
    'first_key'  => 'value',
    'second_key' => 'value',
]);
```

`add` 会覆盖已有的键。若只想在键不存在时才添加，使用 `addIf`。

```php theme={null}
Context::add('key', 'first');
Context::addIf('key', 'second');

Context::get('key');
// "first" — 未被覆盖
```

### increment / decrement — 管理计数器

用于数值增减的专用方法，可通过第二个参数指定增减量。

```php theme={null}
Context::increment('records_added');
Context::increment('records_added', 5);

Context::decrement('records_added');
Context::decrement('records_added', 5);
```

### when — 按条件添加

使用 `when` 可以根据条件为 `true` 或 `false` 分别添加不同的数据。

```php theme={null}
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 — 追加到栈

Context 支持以「栈」的形式保存列表数据。
`push` 会按添加顺序累积数据。

```php theme={null}
Context::push('breadcrumbs', 'first_value');
Context::push('breadcrumbs', 'second_value', 'third_value');

Context::get('breadcrumbs');
// ['first_value', 'second_value', 'third_value']
```

以下是把查询执行历史存入栈的示例：

```php theme={null}
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;

// 在 AppServiceProvider.php 的 boot 方法中注册
DB::listen(function ($event) {
    Context::push('queries', [$event->time, $event->sql]);
});
```

## 读取上下文

### get / all

```php theme={null}
$value = Context::get('key');

// 获取全部
$data = Context::all();
```

### only / except — 获取子集

```php theme={null}
$data = Context::only(['first_key', 'second_key']);

$data = Context::except(['first_key']);
```

### pull / pop — 取出并删除

`pull` 在取值的同时从上下文中删除该键。

```php theme={null}
$value = Context::pull('key');
```

从栈中取出最后一个值使用 `pop`。

```php theme={null}
Context::push('breadcrumbs', 'first_value', 'second_value');

Context::pop('breadcrumbs');
// 'second_value'

Context::get('breadcrumbs');
// ['first_value']
```

### remember — 不存在则设置并返回

```php theme={null}
$permissions = Context::remember(
    'user-permissions',
    fn () => $user->permissions,
);
```

### has / missing — 判断键是否存在

```php theme={null}
if (Context::has('key')) {
    // ...
}

if (Context::missing('key')) {
    // ...
}
```

<Info>
  即使存的是 `null`，`has` 也会返回 `true`。它只判断键是否已注册。
</Info>

## 删除上下文

使用 `forget` 删除键。

```php theme={null}
Context::add(['first_key' => 1, 'second_key' => 2]);

Context::forget('first_key');

Context::all();
// ['second_key' => 2]

// 一次删除多个
Context::forget(['first_key', 'second_key']);
```

## 作用域上下文

使用 `scope` 可以在闭包执行期间临时修改上下文，闭包结束后自动恢复原状态。
在测试或局部处理中希望暂时向日志中附加信息时非常方便。

```php theme={null}
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Log;

Context::add('trace_id', 'abc-999');
Context::addHidden('user_id', 123);

Context::scope(
    function () {
        Context::add('action', 'adding_friend');

        $userId = Context::getHidden('user_id');

        Log::debug("Adding user [{$userId}] to friends list.");
        // Adding user [987] to friends list.  {"trace_id":"abc-999","user_name":"taylor_otwell","action":"adding_friend"}
    },
    data: ['user_name' => 'taylor_otwell'],
    hidden: ['user_id' => 987],
);

// scope 结束后已恢复
Context::all();
// ['trace_id' => 'abc-999']

Context::allHidden();
// ['user_id' => 123]
```

<Warning>
  在 scope 内修改了对象的话，该修改在 scope 外也会保留。使用基本类型时不会有此问题。
</Warning>

## Hidden Context

对于不希望输出到日志的数据（密码、API 密钥、个人识别信息等），可存放到 Hidden Context 中。
普通的 `get` 无法读取，需通过 `getHidden` 等专用方法访问。

```php theme={null}
use Illuminate\Support\Facades\Context;

Context::addHidden('key', 'value');

Context::getHidden('key');
// 'value'

Context::get('key');
// null — 普通 get 无法读取
```

Hidden Context 提供与普通 Context 一致的方法集：

```php theme={null}
Context::addHidden(/* ... */);
Context::addHiddenIf(/* ... */);
Context::pushHidden(/* ... */);
Context::getHidden(/* ... */);
Context::pullHidden(/* ... */);
Context::popHidden(/* ... */);
Context::onlyHidden(/* ... */);
Context::exceptHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::missingHidden(/* ... */);
Context::forgetHidden(/* ... */);
```

## 传递到队列任务

当把任务派发到队列时，当前的上下文会自动序列化并包含在任务的 payload 中。
任务执行时会恢复原上下文，因此请求中设置的 `trace_id` 也会自动出现在队列中的日志里。

```php theme={null}
// 在中间件中设置
Context::add('trace_id', Str::uuid()->toString());

// 在控制器中派发任务
ProcessPodcast::dispatch($podcast);
```

```php theme={null}
class ProcessPodcast implements ShouldQueue
{
    use Queueable;

    public function handle(): void
    {
        Log::info('Processing podcast.', ['podcast_id' => $this->podcast->id]);
    }
}
```

```text theme={null}
Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
```

可以看到请求中的 `trace_id` 也出现在了队列上的日志中。

### Dehydrating — 任务发送时的自定义

`Context::dehydrating` 允许在任务发送前修改上下文。
比如，可用于把由 `Accept-Language` 头决定的 locale 传给队列。

```php theme={null}
// AppServiceProvider.php

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'));
    });
}
```

<Warning>
  在 `dehydrating` 回调中请不要使用 `Context` 门面，而是操作回调传入的 `$context` 仓库。
  使用门面会修改当前进程的上下文。
</Warning>

### Hydrated — 任务执行时的恢复

`Context::hydrated` 用于在任务执行前恢复上下文的时机添加处理。
比如，把此前保存的 locale 应用回配置：

```php theme={null}
// AppServiceProvider.php

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'));
        }
    });
}
```

<Warning>
  在 `hydrated` 回调中同样不要使用 `Context` 门面，只操作传入的 `$context` 仓库。
</Warning>

## 小结

<AccordionGroup>
  <Accordion title="Context 与 Log::withContext 的区别">
    |           | `Context`               | `Log::withContext` |
    | --------- | ----------------------- | ------------------ |
    | 作用范围      | 所有日志通道                  | 仅特定通道              |
    | 传递到队列任务   | 自动（Dehydrate / Hydrate） | 不传递                |
    | Hidden 数据 | 支持                      | 不支持                |
    | 用途        | 追踪、分布式系统                | 通道自身的元数据           |
  </Accordion>

  <Accordion title="适合放在 Hidden Context 的数据">
    Hidden Context 不会输出到日志，因此适合安全地存放：

    * 会话 ID 或用户 ID（不希望进日志时）
    * API 密钥或认证令牌
    * locale 或配置项（需要传递给队列但不需要出现在日志中）
    * 内部标志或状态
  </Accordion>

  <Accordion title="Dehydrate / Hydrate 的常见用法">
    1. **locale 传递**：`dehydrating` 时把 `app.locale` 存入 Hidden Context，`hydrated` 时通过 `Config::set` 恢复。
    2. **认证信息传播**：使队列任务也能访问请求时已认证的用户信息。
    3. **租户 ID**：在多租户应用中跨队列共享租户标识。
  </Accordion>
</AccordionGroup>


## Related topics

- [会话上下文与过滤](/zh/packages/laravel-copilot-sdk/session-context.md)
- [日志](/zh/logging.md)
- [Laravel AI SDK](/zh/ai-sdk.md)
- [快速开始 - GitHub Copilot SDK for Laravel](/zh/packages/laravel-copilot-sdk/getting-started.md)
- [流式事件](/zh/packages/laravel-copilot-sdk/streaming-events.md)
