【Laravel-海賊王系列】第十四章,Session 解析
Laravel
是完全廢棄了 PHP
官方提供的 Session
服務而自己實現了。
實現機參考文末拓展。
開始,從路由的執行說起
我們從路由呼叫控制器的程式碼來反推比較好理解!
定位到 【Laravel-海賊王系列】第十三章,路由&控制器解析 的程式碼
//這段就是路由呼叫控制器的地方 protected function runRouteWithinStack(Route $route, Request $request) { $shouldSkipMiddleware = $this->container->bound('middleware.disable') && $this->container->make('middleware.disable') === true; // 這裡的 `$middleware` 就有關於 `Session` 啟動的中介軟體 $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route); return (new Pipeline($this->container)) ->send($request) ->through($middleware) ->then(function ($request) use ($route) { return $this->prepareResponse( $request, $route->run() ); }); } 複製程式碼
解析路由通過的中介軟體
這裡通過 gatherRouteMiddleware($route)
這個方法來獲取中介軟體了
public function gatherRouteMiddleware(Route $route) { $middleware = collect($route->gatherMiddleware())->map(function ($name) { return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups); })->flatten(); return $this->sortMiddleware($middleware); } 複製程式碼
上面的程式碼我們分幾步來拆解:
- 先看
$route->gatherMiddleware()
public function gatherMiddleware() { if (! is_null($this->computedMiddleware)) { return $this->computedMiddleware; } return $this->computedMiddleware = array_unique(array_merge( $this->middleware(), $this->controllerMiddleware() ), SORT_REGULAR); } 複製程式碼
這裡主要看 middleware
返回值
public function middleware($middleware = null) { if (is_null($middleware)) { return (array) ($this->action['middleware'] ?? []); } if (is_string($middleware)) { $middleware = func_get_args(); } $this->action['middleware'] = array_merge( (array) ($this->action['middleware'] ?? []), $middleware ); return $this; } 複製程式碼
下圖就是 Illuminate\Routing\Route
的 $this->action
屬性

我們從中解析出 web
字串返回。
-
接著看
return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
這段程式碼主要功能就是從
$this->middleware
和$this->middlewareGroups
中解析出$name
對應的中介軟體。我們上面解析的
web
字串就是傳遞到這裡的$name
那麼
$this->middleware
和$this->middlewareGroups
是什麼?我們先看圖在分析怎麼來的!

這兩個屬性是在核心的建構函式注入的 App\Http\Kernel
繼承了 Illuminate\Foundation\Http\Kernel
在 index.php
中載入的真實核心類
// 這個類沒有建構函式,所以執行了父類的建構函式。 // 排序用的中介軟體組 protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\Authenticate::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Auth\Middleware\Authorize::class, ]; // 不同請求型別的中介軟體組 protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'throttle:60,1', 'bindings', ], ]; // 通用中介軟體組 protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; 複製程式碼
Illuminate\Foundation\Http\Kernel
核心建構函式
public function __construct(Application $app, Router $router) { $this->app = $app; $this->router = $router; $router->middlewarePriority = $this->middlewarePriority;// 注入到了 Router 物件的對應成員中 foreach ($this->middlewareGroups as $key => $middleware) { $router->middlewareGroup($key, $middleware); // 注入到了 Router 物件的對應成員中 } foreach ($this->routeMiddleware as $key => $middleware) { $router->aliasMiddleware($key, $middleware); // 注入到了 Router 物件的對應成員中 } } 複製程式碼
返回值如下圖

- 最後還有個排序
return $this->sortMiddleware($middleware);
protected function sortMiddleware(Collection $middlewares) { return (new SortedMiddleware($this->middlewarePriority, $middlewares))->all(); } 複製程式碼
這就是按照上面解析的 $this-middlewarePriority
的優先順序進行排序。
分析 StartSession
StartSession
預覽
上一步可以看到在 web
請求下我們是會預設通過 StartSession
中介軟體的。
我們來分析 StartSession
,先看看整個類都有什麼,然後逐句解析。
<?php namespace Illuminate\Session\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Session\SessionManager; use Illuminate\Contracts\Session\Session; use Illuminate\Session\CookieSessionHandler; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; class StartSession { protected $sessionHandled = false; public function __construct(SessionManager $manager) { // 通過 SessionManager 來管理驅動,方便支援多種形式儲存 $this->manager = $manager; } public function handle($request, Closure $next) { $this->sessionHandled = true; if ($this->sessionConfigured()) { $request->setLaravelSession( $session = $this->startSession($request) ); $this->collectGarbage($session); } $response = $next($request); if ($this->sessionConfigured()) { $this->storeCurrentUrl($request, $session); $this->addCookieToResponse($response, $session); } return $response; } public function terminate($request, $response) { if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) { $this->manager->driver()->save(); } } protected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); } public function getSession(Request $request) { return tap($this->manager->driver(), function ($session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); } protected function collectGarbage(Session $session) { $config = $this->manager->getSessionConfig(); if ($this->configHitsLottery($config)) { $session->getHandler()->gc($this->getSessionLifetimeInSeconds()); } } protected function configHitsLottery(array $config) { return random_int(1, $config['lottery'][1]) <= $config['lottery'][0]; } protected function storeCurrentUrl(Request $request, $session) { if ($request->method() === 'GET' && $request->route() && ! $request->ajax() && ! $request->prefetch()) { $session->setPreviousUrl($request->fullUrl()); } } protected function addCookieToResponse(Response $response, Session $session) { if ($this->usingCookieSessions()) { $this->manager->driver()->save(); } if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) { $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), $this->getCookieExpirationDate(), $config['path'], $config['domain'], $config['secure'] ?? false, $config['http_only'] ?? true, false, $config['same_site'] ?? null )); } } protected function getSessionLifetimeInSeconds() { return ($this->manager->getSessionConfig()['lifetime'] ?? null) * 60; } protected function getCookieExpirationDate() { $config = $this->manager->getSessionConfig(); return $config['expire_on_close'] ? 0 : Carbon::now()->addMinutes($config['lifetime']); } protected function sessionConfigured() { return ! is_null($this->manager->getSessionConfig()['driver'] ?? null); } protected function sessionIsPersistent(array $config = null) { $config = $config ?: $this->manager->getSessionConfig(); return ! in_array($config['driver'], [null, 'array']); } protected function usingCookieSessions() { if ($this->sessionConfigured()) { return $this->manager->driver()->getHandler() instanceof CookieSessionHandler; } return false; } } 複製程式碼
這是整個 StartSession
的中介軟體
構造方法
public function __construct(SessionManager $manager) { // 通過 Illuminate\Session\SessionManager 來管理驅動,方便支援多種形式儲存 $this->manager = $manager; } 複製程式碼
解析 session
例項
接著看中介軟體的 handle()
方法,核心就是獲取 session
物件然後設定到 $request
物件中
public function handle($request, Closure $next) { $this->sessionHandled = true; // 通過 config('session.driver'), 框架預設是 'file' if ($this->sessionConfigured()) { $request->setLaravelSession( $session = $this->startSession($request) ); $this->collectGarbage($session); } $response = $next($request); if ($this->sessionConfigured()) { $this->storeCurrentUrl($request, $session); $this->addCookieToResponse($response, $session); } return $response; } 複製程式碼
我們先通過 $request->setLaravelSession($session = $this->startSession($request) );
獲取一個 session
物件
追蹤程式碼 $this->startSession($request)
protected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); } 複製程式碼
繼續追蹤 $this->getSession($request)
public function getSession(Request $request) { return tap($this->manager->driver(), function ($session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); } 複製程式碼
這裡要追蹤 $this->manager->driver()
返回的是什麼物件!
我們直接呼叫了 Illuminate\Support\Manager
這個抽象類的 driver
方法
public function driver($driver = null) { $driver = $driver ?: $this->getDefaultDriver(); if (is_null($driver)) { throw new InvalidArgumentException(sprintf( 'Unable to resolve NULL driver for [%s].', static::class )); } if (! isset($this->drivers[$driver])) { $this->drivers[$driver] = $this->createDriver($driver); } return $this->drivers[$driver]; } 複製程式碼
這裡只需要關注
protected function createDriver($driver) { if (isset($this->customCreators[$driver])) { return $this->callCustomCreator($driver); } else { $method = 'create'.Str::studly($driver).'Driver'; if (method_exists($this, $method)) { return $this->$method(); } } throw new InvalidArgumentException("Driver [$driver] not supported."); } 複製程式碼
到了這裡其實就是得到一個 $method
方法那麼框架其實最後呼叫了 createFileDriver()
這裡其實就是工廠模式根據配置來載入對應驅動,即使更換 redis
驅動只不過變成 createRedisDriver()
而已。
回到一開始建構函式注入的 Illuminate\Session\SessionManager
物件
protected function createFileDriver() { return $this->createNativeDriver(); } 複製程式碼
繼續展開
protected function createNativeDriver() { $lifetime = $this->app['config']['session.lifetime']; return $this->buildSession(new FileSessionHandler( $this->app['files'], $this->app['config']['session.files'], $lifetime )); } 複製程式碼
那麼實際最後獲取一個 Illuminate\Session\FileSessionHandler
物件
真實的驅動類
我們總算得到了直接和儲存層互動的驅動
展開結構
<?php namespace Illuminate\Session; use SessionHandlerInterface; use Illuminate\Support\Carbon; use Symfony\Component\Finder\Finder; use Illuminate\Filesystem\Filesystem; class FileSessionHandler implements SessionHandlerInterface { protected $files; protected $path; protected $minutes; public function __construct(Filesystem $files, $path, $minutes) { $this->path = $path; $this->files = $files; $this->minutes = $minutes; } // 為了閱讀體驗就不展開裡面的程式碼,實際功能就是呼叫儲存層進行增刪改查 public function open($savePath, $sessionName){ ... } public function close(){ ... } public function read($sessionId){ ... } public function write($sessionId, $data){ ... } public function destroy($sessionId){ ... } public function gc($lifetime){ ... } } 複製程式碼
最後一段程式碼
protected function buildSession($handler) { if ($this->app['config']['session.encrypt']) { return $this->buildEncryptedSession($handler); } return new Store($this->app['config']['session.cookie'], $handler); } 複製程式碼
最後根據加密配置返回一個 Illuminate\Session\EncryptedStore
或者 Illuminate\Session\Store
物件
這個 Store
我們看看建構函式就會了解他的功能!
public function __construct($name, SessionHandlerInterface $handler, $id = null) { $this->setId($id); $this->name = $name; $this->handler = $handler; } 複製程式碼
這個接收了 SessionHandler
就相當於擁有了和資料儲存互動的能力,這個類對使用者層提供了
和 session
互動的所有 api
,對使用者來說隱藏了底層的驅動實現。
好了,回到開始的部分
protected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); } 複製程式碼
我們已經得到了 $session
這個物件,接著就是呼叫 setRequestOnHandler
和 start()
方法
這裡我們跳過 setRequestOnHandler
因為這段程式碼是在針對使用 Cookie
來當驅動的時候設定的,基本不會使用。
直接看 start()
方法
public function start() { $this->loadSession(); if (! $this->has('_token')) { $this->regenerateToken(); } return $this->started = true; } 複製程式碼
繼續看
protected function loadSession() { $this->attributes = array_merge($this->attributes, $this->readFromHandler()); } 複製程式碼
繼續看
protected function readFromHandler() { if ($data = $this->handler->read($this->getId())) { $data = @unserialize($this->prepareForUnserialize($data)); if ($data !== false && ! is_null($data) && is_array($data)) { return $data; } } return []; } 複製程式碼
這裡的程式碼就是直接通過驅動傳入 SessionId
然後獲取存入的資料
之後賦值給 Illuminate\Session\Store
的 $this->attributes
因此 Illuminate\Session\Store
物件才是真正和我們打交道的物件!
使用者層的體驗 Illuminate\Session\Store
通過上面的分析,我麼知道 Laravel
遮蔽了資料驅動層,直接向上層
提供了 Store
物件來實現對整個 Session
的呼叫,使用者不需要在關心
底層的實現邏輯,只需要按照配置設定好驅動然後呼叫 Store
中提供的方法即可!
最後我們所有的 get()
set()
flush()
等等操作只不過是 Store
提供的服務。
拓展--實現 SessionHandlerInterface
關於實現 implements SessionHandlerInterface
其實 PHP
的針對自定義 Session
提供了預留介面,要自己拓展就必須實現這個介面中定義的方法,
在 PHP
底層會通過這幾個方法將 Session ID
傳遞進來。
結語
通過本章我們要了解幾個重點
-
StartSession
中介軟體的啟動過程 (Kernel
中配置) -
Session
驅動的載入方式 (通過SessionManager
工廠載入) - 使用者最後針對
Session
的所有操作是由Illuminate\Session\Store
物件提供 -
PHP
提供的SessionHandlerInterface
來拓展自定義Session
這是底層互動,必須實現。