v13.39.0 官方源码的确认。基本操作请参阅 Session,服务提供者的自动加载请参阅包的自动发现。
extend 接收的是处理器而不是 Store
Session::extend() 的回调返回的是实现了 PHP SessionHandlerInterface 的存储处理器。它不是直接返回实现 Illuminate\Contracts\Session\Session 的 Store 或缓存 Repository 的 API。
SessionManager::callCustomCreator() 会将回调的返回值传给 buildSession(),并根据 session.encrypt 用 Store 或 EncryptedStore 进行包装。此时还会应用 Cookie 名称和 session.serialization。
在常规 HTTP 处理中,StartSession 中间件负责管理 Session 的启动与保存。为了提供处理器,包本身无需调用 session_start() 或 session_set_save_handler()。
分别理解存储契约
实现自定义后端时,需要满足以下职责。
由于
Store 负责序列化与还原,处理器中不要再次对 Session 数组执行 serialize() 或 json_encode()。启用加密时,也直接保存经 EncryptedStore 处理后的字符串。
在 boot 中只注册创建处理
下面是复用内置CacheBasedSessionHandler 的最小示例。它并非实现新的存储协议,而是展示如何注册使用命名缓存存储的自定义驱动。假设 Acme\SessionBridge\ 通过 Composer 的 PSR-4 映射到 src/。
在 src/SessionBridgeServiceProvider.php 中准备服务提供者。
Session::extend() 会获取 SessionManager 并注册创建处理,但此时不会解析回调内的缓存存储。存储位置在该驱动首次被选中时才会创建。不要在服务提供者的 boot() 中执行连接检查或数据保存。
使用自定义的 SessionHandlerInterface 实现时,替换回调返回的处理器即可。如果连 new Store(...) 也在回调中完成,就会与 SessionManager 期望的返回值不一致。
- 服务提供者按常规的非延迟提供者加载。
- 在 Session 首次被使用之前完成注册。如果其他服务提供者会先解析自定义驱动,还需确认加载顺序。
- 驱动名称使用包专属的名称。同名的
extend()会覆盖创建处理;如果注册内置驱动的名称,则会优先于内置实现。
由使用方应用显式选择
使用者修改现有config/session.php 中的对应键,其他配置保持不变。
config/cache.php 现有的 stores 数组中添加命名存储。下面是使用 Redis 连接 default 的示例。Redis 的连接配置与所需客户端由应用方准备。
createRedisDriver(),即使添加 session.connection 或 session.prefix,该回调也不会自动应用它们。
该回调不会修改 Repository 或存储位置的配置,而是直接使用命名存储。测试用的 array 存储在同一应用内也共享同一个存储区域。
包不应无条件改写 session.driver、Cookie 名称或应用全局的缓存前缀。是否采用不与其他用途共享存储区域的设计,以及能否让多台服务器使用同一存储位置,也要与使用者确认。
保持 TTL 的单位与存储格式
CacheBasedSessionHandler 的构造函数以分钟为单位接收保存期限。write() 每次都会将 $minutes * 60 传给缓存 Repository 的 put(),因此在上面的示例中,每次写入都会刷新 TTL。gc() 返回 0,将过期处理交给缓存端。
另一方面,StartSession 传给处理器 gc($lifetime) 的期限以秒为单位。不要将 session.lifetime 的值直接当作秒来保存。
此外,以下变更并不只是修改配置名称那么简单。
如果在包更新中修改这些内容,需要考虑重新认证的提示、是否需要迁移处理,以及新旧版本混合存在的部署。与包的版本兼容性管理一样,存储格式和配置键也应纳入需要维护的公开规范。
已创建的 Store 不会仅因配置变更而被替换
SessionManager 的父类Manager 会按名称保留已创建的驱动。此外,SessionServiceProvider 将 session.store 注册为 singleton。
forgetDrivers() 也不是删除 Session 数据的 API。在运行中的请求里重新创建 Store,会导致与已读取的数据或其他引用之间不一致。
本例中的 CacheManager 也会保留命名存储。如果修改了其连接配置,仅重新解析 SessionManager 是不够的。
在使用配置缓存的部署中,需要重建缓存,并重启长时间运行的 worker,使其加载新的代码和配置。应用重启与已保存数据的迁移是两项不同的工作。
Session 存储与请求锁是两回事
仅实现自定义处理器,并不能让针对同一 Session 的并行请求串行化。StartSession 的阻塞功能使用通过 session.block_store 选择的缓存存储的锁,它不一定与 Session 本体的存储位置相同。
通过 Route::post(...)->block() 等方式控制时,需要另外验证支持锁的缓存存储、多台服务器之间的共享,以及等待时间与持有时间。包不应擅自启用应用全局的阻塞配置。
测试到保存与重新加载
在使用 Orchestra Testbench 的包中,应准备加载服务提供者并实际选择自定义驱动的测试。下面是tests/SessionDriverTest.php 的示例。测试用的 array 缓存不使用外部连接。
array 用作生产环境的共享存储位置。
在发布自定义处理器之前,还应添加以下测试。
- 未保存或已过期的 ID 返回空字符串
- 保存的字符串能原样还原,删除后无法再次获取
- TTL 的单位与刷新,以及真实后端中的过期
- 读取、写入、删除失败时,得到约定的返回值或异常
- 启用
session.encrypt时的往返,以及所支持的序列化格式 - 仅加载服务提供者不会访问存储位置,也不会抢占内置驱动的选择
- 如有需要,经过真实
web中间件与 Cookie 的 HTTP 测试,以及并行请求的锁
相关页面
Session
了解 Session 的读写、闪存数据以及 ID 的重新生成。
Manager 的内部实现
了解自定义驱动的创建与保留,以及向默认驱动的委托。
包的自定义缓存存储
了解作为存储位置的缓存 Store 与 Repository 的分工,以及 TTL 契约。
包服务的生命周期
了解长时间运行的 worker 中保留的依赖与状态隔离。
一手资料
- 官方 HTTP Session 文档:自定义驱动与阻塞
- 官方包开发文档
- SessionManager:自定义创建处理与 Store 的构建
- Store:读取、序列化、保存、ID 的重新生成
- CacheBasedSessionHandler:TTL 转换与存储契约
- CacheManager:命名存储与 Redis 配置
- Manager:extend、driver、forgetDrivers
- SessionServiceProvider:Manager 与 session.store 的 singleton 注册
- StartSession:保存处理、GC、基于缓存的锁
- 官方 CacheBasedSessionHandlerTest:未保存、TTL、删除的验证