什么是 Context
Laravel 的 Context 功能提供了一种在请求、队列任务、命令之间跨越执行边界记录与共享信息的机制。
通过 Illuminate\Support\Facades\Context 门面添加的信息,会自动附带到应用写出的所有日志条目中。
这样就能明确区分某次日志调用附带的信息与 Context 保存的共享信息。
在使用队列或分布式架构进行追踪时尤其有用。
上下文的传播流程
基本用法
最典型的用法是在中间件中设置 trace_id。之后所有的日志都会自动包含它。
创建中间件
php artisan make:middleware AddContext
向 Context 添加追踪 ID
<? 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 );
}
}
注册中间件
在 bootstrap/app.php 中作为全局中间件注册。 -> withMiddleware ( function ( Middleware $middleware ) {
$middleware -> append ( \App\Http\Middleware\ AddContext :: class );
})
配置好后,控制器或服务写入的日志会自动附带 url 和 trace_id。
Log :: info ( 'User authenticated.' , [ 'auth_id' => Auth :: id ()]);
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
写入上下文
add — 添加值
use Illuminate\Support\Facades\ Context ;
Context :: add ( 'key' , 'value' );
// 一次添加多个
Context :: add ([
'first_key' => 'value' ,
'second_key' => 'value' ,
]);
add 会覆盖已有的键。若只想在键不存在时才添加,使用 addIf。
Context :: add ( 'key' , 'first' );
Context :: addIf ( 'key' , 'second' );
Context :: get ( 'key' );
// "first" — 未被覆盖
increment / decrement — 管理计数器
用于数值增减的专用方法,可通过第二个参数指定增减量。
Context :: increment ( 'records_added' );
Context :: increment ( 'records_added' , 5 );
Context :: decrement ( 'records_added' );
Context :: decrement ( 'records_added' , 5 );
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 — 追加到栈
Context 支持以「栈」的形式保存列表数据。
push 会按添加顺序累积数据。
Context :: push ( 'breadcrumbs' , 'first_value' );
Context :: push ( 'breadcrumbs' , 'second_value' , 'third_value' );
Context :: get ( 'breadcrumbs' );
// ['first_value', 'second_value', 'third_value']
以下是把查询执行历史存入栈的示例:
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
$value = Context :: get ( 'key' );
// 获取全部
$data = Context :: all ();
only / except — 获取子集
$data = Context :: only ([ 'first_key' , 'second_key' ]);
$data = Context :: except ([ 'first_key' ]);
pull / pop — 取出并删除
pull 在取值的同时从上下文中删除该键。
$value = Context :: pull ( 'key' );
从栈中取出最后一个值使用 pop。
Context :: push ( 'breadcrumbs' , 'first_value' , 'second_value' );
Context :: pop ( 'breadcrumbs' );
// 'second_value'
Context :: get ( 'breadcrumbs' );
// ['first_value']
remember — 不存在则设置并返回
$permissions = Context :: remember (
'user-permissions' ,
fn () => $user -> permissions ,
);
has / missing — 判断键是否存在
if ( Context :: has ( 'key' )) {
// ...
}
if ( Context :: missing ( 'key' )) {
// ...
}
即使存的是 null,has 也会返回 true。它只判断键是否已注册。
删除上下文
使用 forget 删除键。
Context :: add ([ 'first_key' => 1 , 'second_key' => 2 ]);
Context :: forget ( 'first_key' );
Context :: all ();
// ['second_key' => 2]
// 一次删除多个
Context :: forget ([ 'first_key' , 'second_key' ]);
作用域上下文
使用 scope 可以在闭包执行期间临时修改上下文,闭包结束后自动恢复原状态。
在测试或局部处理中希望暂时向日志中附加信息时非常方便。
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]
在 scope 内修改了对象的话,该修改在 scope 外也会保留。使用基本类型时不会有此问题。
Hidden Context
对于不希望输出到日志的数据(密码、API 密钥、个人识别信息等),可存放到 Hidden Context 中。
普通的 get 无法读取,需通过 getHidden 等专用方法访问。
use Illuminate\Support\Facades\ Context ;
Context :: addHidden ( 'key' , 'value' );
Context :: getHidden ( 'key' );
// 'value'
Context :: get ( 'key' );
// null — 普通 get 无法读取
Hidden Context 提供与普通 Context 一致的方法集:
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 也会自动出现在队列中的日志里。
// 在中间件中设置
Context :: add ( 'trace_id' , Str :: uuid () -> toString ());
// 在控制器中派发任务
ProcessPodcast :: dispatch ( $podcast );
class ProcessPodcast implements ShouldQueue
{
use Queueable ;
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"}
可以看到请求中的 trace_id 也出现在了队列上的日志中。
Dehydrating — 任务发送时的自定义
Context::dehydrating 允许在任务发送前修改上下文。
比如,可用于把由 Accept-Language 头决定的 locale 传给队列。
// 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' ));
});
}
在 dehydrating 回调中请不要使用 Context 门面,而是操作回调传入的 $context 仓库。
使用门面会修改当前进程的上下文。
Hydrated — 任务执行时的恢复
Context::hydrated 用于在任务执行前恢复上下文的时机添加处理。
比如,把此前保存的 locale 应用回配置:
// 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' ));
}
});
}
在 hydrated 回调中同样不要使用 Context 门面,只操作传入的 $context 仓库。
Context 与 Log::withContext 的区别
ContextLog::withContext作用范围 所有日志通道 仅特定通道 传递到队列任务 自动(Dehydrate / Hydrate) 不传递 Hidden 数据 支持 不支持 用途 追踪、分布式系统 通道自身的元数据
Hidden Context 不会输出到日志,因此适合安全地存放:
会话 ID 或用户 ID(不希望进日志时)
API 密钥或认证令牌
locale 或配置项(需要传递给队列但不需要出现在日志中)
内部标志或状态
Dehydrate / Hydrate 的常见用法
locale 传递 :dehydrating 时把 app.locale 存入 Hidden Context,hydrated 时通过 Config::set 恢复。
认证信息传播 :使队列任务也能访问请求时已认证的用户信息。
租户 ID :在多租户应用中跨队列共享租户标识。