分析 Session(1)

今天要講的是與預設 middleware 相關的另一個元件--Session。這個元件應該是到目前為止,最多類別的元件。

在看類別圖之前,我們先從 SessionServiceProvider 了解裡面有哪些相關的類別需要初始化:

原本是分不同的方法各別呼叫 singleton,這裡刻意使用依序呼叫的方法來呈現。

$this->app->singleton('session', function ($app) {
return new SessionManager($app);
});

$this->app->singleton('session.store', function ($app) {
return $app->make('session')->driver();
});

$this->app->singleton(StartSession::class);

從這裡可以知道,SessionManager 是最主要的核心類別,StartSession 則是 middleware。

類別圖

為了版面簡潔,Illuminate\Session 開頭的類別,都有忽略。除非是跟其他元件有關聯。另外為了表示這元件的內聚關係,有忽略一些靜態呼叫的類別,如 Illuminate\Support\Arr 類別。

@startuml
abstract class Illuminate\Support\Manager {
# drivers : array, Store instance
}

interface SessionHandlerInterface
interface Illuminate\Contracts\Cache\Repository
interface Illuminate\Contracts\Encryption\Encrypter
interface Illuminate\Contracts\Session\Session

Illuminate\Support\Manager <|-left- SessionManager
Illuminate\Support\Manager o-- Store
Store .right.|> Illuminate\Contracts\Session\Session
EncryptedStore -|> Store
SessionManager --> EncryptedStore : new instance
SessionManager --> Store : new instance
Store o-- SessionHandlerInterface
EncryptedStore o-- Illuminate\Contracts\Encryption\Encrypter
CacheBasedSessionHandler .up.|> SessionHandlerInterface
CacheBasedSessionHandler o-down- Illuminate\Contracts\Cache\Repository
CookieSessionHandler .up.|> SessionHandlerInterface
DatabaseSessionHandler .up.|> SessionHandlerInterface
FileSessionHandler .up.|> SessionHandlerInterface
NullSessionHandler .up.|> SessionHandlerInterface
@enduml

從這張圖可以了解類別間關係,比方說:

SessionManager

雖然有很多關係連結,不過就先從 SessionManager 類別開始吧!SessionManager 繼承了 Illuminate\Support\Manager

Manager 的用途很特別,通常我們在不同的場景會使用不同實作時,如 DB 在測試會使用 SQLite,上線會使用 MySQL,這時為符合 open-closed principle,通常我們會選擇 strategy pattern。Manager 就是一個管理 strategy 實例的抽象類。

一開始初始化,當然會需要 Container,因為後面要產生實例的時候會需要它。而今天一開始在講 service provider 時,有看到這段程式碼:

$this->app->singleton('session.store', function ($app) {
return $app->make('session')->driver();
});

這是取得實例的實作,來看看 Manager::driver()

public function driver($driver = null)
{
// 如果是 null 就使用預設的 driver
$driver = $driver ?: $this->getDefaultDriver();

// 如果 default 是 null ....別鬧了!
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}

// 如果還沒建構的話,就建構起來。這裡使用了 registry of singleton pattern 來實作單例
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}

return $this->drivers[$driver];
}

從這段程式碼和 session.store 的建構方法可以知道,SessionManager 只會使用 default driver 而已(也就是 $driver 恆為 null)。Manager 定義 getDefaultDriver() 是抽象方法,我們先來看 SessionManager 是如何實作的:

public function getDefaultDriver()
{
return $this->app['config']['session.driver'];
}

這就是 Laravel config/session.php 的 driver 設定。從這個設定檔的註解也可以知道,driver 有非常多種,如 filedatabasememcached 等,這也剛好對應類別圖的 SessionHandlerInterface 實作。

如果設定是 file 的話,driver 又是怎麼被建構出來的呢,接著繼續看 createDriver()

protected function createDriver($driver)
{
// 如果有設定自定義建構的話,就使用
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
// 呼叫 createFileDriver 建構
$method = 'create'.Str::studly($driver).'Driver';

if (method_exists($this, $method)) {
return $this->$method();
}
}

// 都不是的話,只好丟例外
throw new InvalidArgumentException("Driver [$driver] not supported.");
}

這裡的 createFileDriver 或是其他 driver 會是由子類別實作了,來繼續看 file driver 怎麼做的。

protected function createFileDriver()
{
return $this->createNativeDriver();
}

protected function createNativeDriver()
{
// 取得設定的 session.lifetime
$lifetime = $this->app['config']['session.lifetime'];

// 建構實例
return $this->buildSession(new FileSessionHandler(
$this->app['files'], $this->app['config']['session.files'], $lifetime
));
}

這裡的 $this->app['files']Illuminate\Filesystem\Filesystem 實例,而 session.filessession.lifetime 則是設定。

其他 handler 的分析方法也是依此類推,就先略過了。

FileSessionHandler 實作了 SessionHandlerInterface,這也是 PHP 提供的介面,搭配 session_set_save_handler() 可以用在自定義內建 session 的實作,也就是 $_SESSION 實際背後會處理細節,是可以自定義的。

換句話說,Laravel 所實作的五個 handler 不僅可以用在 SessionManager 上,也可以用在 PHP 內建的 $_SESSION 上,會這樣設計是有原因的,後面會再提到。

這五個實作都有一個共同特色,就是下面這兩個方法都是回傳 true

public function open($savePath, $sessionName)
{
return true;
}

public function close()
{
return true;
}

其他實作則依不同的 handler 有不同的存取方法,如 CacheBasedSessionHandler 就很好理解:

public function read($sessionId)
{
// 從 cache 實例拿資料,預設會是空字串
return $this->cache->get($sessionId, '');
}

public function write($sessionId, $data)
{
// 寫資料
return $this->cache->put($sessionId, $data, $this->minutes);
}

public function destroy($sessionId)
{
// 移除 session 資料
return $this->cache->forget($sessionId);
}

public function gc($lifetime)
{
// 不需做事,因為 Cache 實作如 Redis 都有 expire time
return true;
}

既然一開始有提到 SessionManager 不處理資料,實際處理資料的是 Store,或者說是 Illuminate\Contracts\Session\Session,Laravel 又是如何使用它的呢?從類別圖與上面的程式碼分析可以猜得出來,Laravel 自幹了 Session 處理機制,但它又是如何知道進來的 request 是來自同一個 client 呢?

這些秘密就在 StartSession 這個 middleware 裡,明天再繼續分析。