本篇從Session |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區入手,結合laravel源碼分析HTTP會話機制。
背景知識
在php原生語言中,我們可以使用$_SESSION全局數組來操作session,並且默認情況下,PHP使用內置的文件來保存session在請求間的數據。並且,從PHP4開始,可以使用PHP: session_set_save_handler - Manual函數來設置自定義會話函數:
session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid [, callable $validate_sid [, callable $update_timestamp ]]] ) : bool
通過該函數,我們可以設置session的打開、關閉、讀、寫、銷燬、垃圾回收操作,還可以定義創建sessionId、驗證sessionId和更新session時間的規則。自PHP5.4開始,可以使用面向對象的方式來註冊自定義會話存儲函數:
session_set_save_handler ( object $sessionhandler [, bool $register_shutdown = TRUE ] ) : bool
使用面向對象的方式需要我們自定義一個實現PHP: SessionHandlerInterface - Manual接口的類:
SessionHandlerInterface {
/* 方法 */
abstract public close ( void ) : bool
abstract public destroy ( string $session_id ) : bool
abstract public gc ( int $maxlifetime ) : int
abstract public open ( string $save_path , string $session_name ) : bool
abstract public read ( string $session_id ) : string
abstract public write ( string $session_id , string $session_data ) : bool
}
一種更好的方法是繼承PHP: SessionHandler - Manual類:
SessionHandler implements SessionHandlerInterface , SessionIdInterface {
/* 方法 */
public close ( void ) : bool
public create_sid ( void ) : string
public destroy ( string $session_id ) : bool
public gc ( int $maxlifetime ) : int
public open ( string $save_path , string $session_name ) : bool
public read ( string $session_id ) : string
public write ( string $session_id , string $session_data ) : bool
}
通過PHP語音提供的這種擴展方式,我們自己就可以方便的寫出支持文件、數據庫、Redis、Memcache等存儲介質的http會話存儲方式。在本篇文章結束後,可以發現laravel中session的持久化採用了這種模式,但會話的使用並不是那麼原生。
簡介
由於 HTTP 驅動的應用程序是無狀態的,Session 提供了一種在多個請求之間存儲有關用戶的信息的方法。Laravel 通過同一個可讀性強的 API 處理各種自帶的 Session 後臺驅動程序。支持諸如比較熱門的 Memcached、Redis 和開箱即用的數據庫等常見的後臺驅動程序。
對應於每一種存儲介質,laravel提供了對應的SessionHandler類。
Session driver 的配置選項定義了每個請求存儲 Session 數據的位置。Laravel 自帶了幾個不錯且可開箱即用的驅動:
- file - 將 Session 保存在 storage/framework/sessions 中。
- cookie - Session 保存在安全加密的 Cookie 中。
- database - Session 保存在關係型數據庫中。
- memcached / redis - Sessions 保存在其中一個快速且基於緩存的存儲系統中。
- array - Sessions 保存在 PHP 數組中,不會被持久化。
NullSessionHandler
當配置session驅動爲array時,會使用NullSessionHandler
類來存儲session。NullSessionHandler
所有方法中只有一行代碼:return true;
。所以使用array作爲session驅動,session中的數據不存持久化,只會在內存中保存,當php執行結束後,session中的數據會全部丟失。如果一些數據只需要在當前請求中使用,可以使用array驅動。FileSessionHandler
當配置session驅動爲file時,會使用FileSessionHandler
類來存儲session。file驅動也被稱爲native驅動,PHP默認就是使用文件來保存session數據的。FileSessionHandler
使用FileSystem
來操作文件,open
和close
方法只是return true
。read
和write
方法執行session數據的加載和存儲。read
方法以sessionId
爲文件名創建session文件。destroy
方法會刪除指定sessionId
的文件,gc
方法會刪除過期的session文件。`CookieSessionHandler
當配置session驅動爲cookie時,會使用CookieSessionHandler
類來存儲session。CookieSessionHandler
使用Cookie
來把操作數據。open
和close
方法只是return true
。read
方法根據sessionId
從Request
中的cookie中加載數據,write
方法把session中的數據寫入cookie中,destroy
方法會刪除指定sessionId
的數據,gc
方法直接return true
,數據的過期清除由cookie處理。CacheBasedSessionHandler
當配置session驅動爲memcached/redis/apc時,會使用CacheBasedSessionHandler
類存儲session。使用redis作爲session驅動時,使用config/session.php
中配置的connection
設置連接;使用memcached/apc作爲session驅動時,使用config/session.php
中配置的store
設置緩存的驅動(與config/cache.php
中的stores
對應)。CacheBasedSessionHandler
使用laravel的Cache
來存儲數據(緩存系統 |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區)。open
和close
方法只是retun true
。read
和write
直接從cache中讀取、保存數據;destory
調用cache
的forget
方法清除數據;
gc
方法直接return true
,清除過期數據由cache自動執行。DatabaseSessionHandler
當配置session驅動爲database時,會使用DatabaseSessionHandler
類存儲session。
使用Session
session使用方式
Laravel 中處理 Session 數據有兩種主要方法:全局輔助函數 session 和通過一個 Request 實例。
除了這兩種方法還可以使用Session Facade。
- session輔助方法
if (! function_exists('session')) {
/**
* Get / set the specified session value.
*
* If an array is passed as the key, we will assume you want to set an array of values.
*
* @param array|string $key
* @param mixed $default
* @return mixed|\Illuminate\Session\Store|\Illuminate\Session\SessionManager
*/
function session($key = null, $default = null)
{
if (is_null($key)) {
return app('session');
}
if (is_array($key)) {
return app('session')->put($key);
}
return app('session')->get($key, $default);
}
}
可以發現session是通過app('session')
獲取的。
- 通過Request實例
<?php
namespace Illuminate\Http;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
...
/**
* Get the session associated with the request.
*
* @return \Illuminate\Session\Store
*
* @throws \RuntimeException
*/
public function session()
{
if (! $this->hasSession()) {
throw new RuntimeException('Session store not set on request.');
}
return $this->getSession();
}
...
}
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation;
class Request
{
...
/**
* Gets the Session.
*
* @return SessionInterface|null The session
*/
public function getSession()
{
$session = $this->session;
if (!$session instanceof SessionInterface && null !== $session) {
$this->setSession($session = $session());
}
if (null === $session) {
@trigger_error(sprintf('Calling "%s()" when no session has been set is deprecated since Symfony 4.1 and will throw an exception in 5.0. Use "hasSession()" instead.', __METHOD__), E_USER_DEPRECATED);
// throw new \BadMethodCallException('Session has not been set');
}
return $session;
}
...
}
從Request實例中通過session()
獲取session。
- Session Facade
在Facades |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區Facade類參考表中可以找到Session Facade類及對應的底層類。
其實Facade也是使用類app('session')
的方式,過程可以參考:laravel核心構架——DB Facade_php_szuaudi的博客-CSDN博客
session相關方法
session相關方法主要定義在Store
類中:Illuminate\Session\Store | Laravel API
Session相關類
app('session')
返回的類型是Illuminate\Session\SessionManager
,session相關的操作都會直接作用於SessionManager
類的實例上,session的大量方法在Store
類中定義,通過SessionManager
類代理。
上面類圖中列出了laravel中Session所涉及的幾個重要的類結構。
SessionManaer
類
該類主要根據配置創建相關驅動的SessionHandler和創建Session
對象。
- 獲取配置
/**
* Get the session configuration.
*
* @return array
*/
public function getSessionConfig()
{
return $this->app['config']['session'];
}
- 創建array驅動的Session
/**
* Create an instance of the "array" session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createArrayDriver()
{
return $this->buildSession(new NullSessionHandler);
}
- 創建cookie驅動的Session
/**
* Create an instance of the "array" session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createArrayDriver()
{
return $this->buildSession(new NullSessionHandler);
}
- 創建file驅動的Session
/**
* Create an instance of the file session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createFileDriver()
{
return $this->createNativeDriver();
}
/**
* Create an instance of the file session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createNativeDriver()
{
$lifetime = $this->app['config']['session.lifetime'];
return $this->buildSession(new FileSessionHandler(
$this->app['files'], $this->app['config']['session.files'], $lifetime
));
}
- 創建database驅動的Session
/**
* Create an instance of the database session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createDatabaseDriver()
{
$table = $this->app['config']['session.table'];
$lifetime = $this->app['config']['session.lifetime'];
return $this->buildSession(new DatabaseSessionHandler(
$this->getDatabaseConnection(), $table, $lifetime, $this->app
));
}
/**
* Get the database connection for the database driver.
*
* @return \Illuminate\Database\Connection
*/
protected function getDatabaseConnection()
{
$connection = $this->app['config']['session.connection'];
return $this->app['db']->connection($connection);
}
- 創建apc/memcached/redis驅動的Session
/**
* Create an instance of the APC session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createApcDriver()
{
return $this->createCacheBased('apc');
}
/**
* Create an instance of the Memcached session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createMemcachedDriver()
{
return $this->createCacheBased('memcached');
}
/**
* Create an instance of the Redis session driver.
*
* @return \Illuminate\Session\Store
*/
protected function createRedisDriver()
{
$handler = $this->createCacheHandler('redis');
$handler->getCache()->getStore()->setConnection(
$this->app['config']['session.connection']
);
return $this->buildSession($handler);
}
/**
* Create an instance of a cache driven driver.
*
* @param string $driver
* @return \Illuminate\Session\Store
*/
protected function createCacheBased($driver)
{
return $this->buildSession($this->createCacheHandler($driver));
}
/**
* Create the cache based session handler instance.
*
* @param string $driver
* @return \Illuminate\Session\CacheBasedSessionHandler
*/
protected function createCacheHandler($driver)
{
$store = $this->app['config']->get('session.store') ?: $driver;
return new CacheBasedSessionHandler(
clone $this->app['cache']->store($store),
$this->app['config']['session.lifetime']
);
}
- 創建Session
/**
* Build the session instance.
*
* @param \SessionHandlerInterface $handler
* @return \Illuminate\Session\Store
*/
protected function buildSession($handler)
{
if ($this->app['config']['session.encrypt']) {
return $this->buildEncryptedSession($handler);
}
return new Store($this->app['config']['session.cookie'], $handler);
}
- 設置和獲取默認驅動
/**
* Get the default session driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['session.driver'];
}
/**
* Set the default session driver name.
*
* @param string $name
* @return void
*/
public function setDefaultDriver($name)
{
$this->app['config']['session.driver'] = $name;
}
通過SessionManager
,可以知道session驅動的管理方式。可以通過setDefaultDriver
方法修改Session使用的默認驅動。
Manager
類
Manager
是SessionManager
的父類。
Manager
中有一個重要的方法:__call
/**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
/**
* Get a driver instance.
*
* @param string $driver
* @return mixed
*/
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
// We'll check to see if a creator method exists for the given driver. If not we
// will check for a custom driver creator, which allows developers to create
// drivers using their own customized driver creator Closure to create it.
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.");
}
通過魔術方法_call
,對SessionManager
操作會委託給由buildSession
方法創建的Store
類。
driver
方法會先在drivers
數組中查找驅動,如果沒有再創建驅動。
createDriver
方法會先在自定義的創建器數組customCreators
數組中查找驅動,如果有就調用callCustomCreator
方法創建用戶自定義的方法創建驅動;如果沒有,就根據驅動名稱調用相應驅動創建方法,這些方法在子類SessionManager
中定義。
- 自定義驅動
自定義驅動需要使用extend
方法添加驅動名稱到drivers
數組和添加自定義驅動創建閉包函數到customCreators
數組。
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
* @return $this
*/
public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback;
return $this;
}
/**
* Call a custom driver creator.
*
* @param string $driver
* @return mixed
*/
protected function callCustomCreator($driver)
{
return $this->buildSession(parent::callCustomCreator($driver));
}
自定義的驅動創建閉包函數需要返回一個實現SessionHandler
接口的類的實例給buildSession
類創建Session。我們在子類SessionManager
中能找到該函數的調用。
/**
* Call a custom driver creator.
*
* @param string $driver
* @return mixed
*/
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}
Store
類
在SessionManager
的buildSession
方法中可以看到創建的Session是一個Store
類的實例。對SessionManager
上的調用會委託給Store
類。Store
實現了Session
中聲明的接口。
- 加載session
session開始時,會調用SessionHander
的read
方法把session數據從持久化介質上加載到內存中,以後的操作都會在內存屬性attributes
數組中執行。
/**
* Start the session, reading the data from a handler.
*
* @return bool
*/
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
/**
* Load the session data from the handler.
*
* @return void
*/
protected function loadSession()
{
$this->attributes = array_merge($this->attributes, $this->readFromHandler());
}
/**
* Read the session data from the handler.
*
* @return array
*/
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
/**
* Get a new, random session ID.
*
* @return string
*/
protected function generateSessionId()
{
return Str::random(40);
}
- 重新生成sessionId
/**
* Generate a new session identifier.
*
* @param bool $destroy
* @return bool
*/
public function regenerate($destroy = false)
{
return $this->migrate($destroy);
}
/**
* Generate a new session ID for the session.
*
* @param bool $destroy
* @return bool
*/
public function migrate($destroy = false)
{
if ($destroy) {
$this->handler->destroy($this->getId());
}
$this->setExists(false);
$this->setId($this->generateSessionId());
return true;
}
- 獲取session
/**
* Get an item from the session.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
/**
* Get the value of a given key and then forget it.
*
* @param string $key
* @param string $default
* @return mixed
*/
public function pull($key, $default = null)
{
return Arr::pull($this->attributes, $key, $default);
}
- 保存session
/**
* Push a value onto a session array.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function push($key, $value)
{
$array = $this->get($key, []);
$array[] = $value;
$this->put($key, $array);
}
/**
* Flash a key / value pair to the session.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function flash(string $key, $value = true)
{
$this->put($key, $value);
$this->push('_flash.new', $key);
$this->removeFromOldFlashData([$key]);
}
/**
* Flash a key / value pair to the session for immediate use.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function now($key, $value)
{
$this->put($key, $value);
$this->push('_flash.old', $key);
}
/**
* Reflash all of the session flash data.
*
* @return void
*/
public function reflash()
{
$this->mergeNewFlashes($this->get('_flash.old', []));
$this->put('_flash.old', []);
}
/**
* Reflash a subset of the current flash data.
*
* @param array|mixed $keys
* @return void
*/
public function keep($keys = null)
{
$this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args());
$this->removeFromOldFlashData($keys);
}
- 清除session
/**
* Remove an item from the session, returning its value.
*
* @param string $key
* @return mixed
*/
public function remove($key)
{
return Arr::pull($this->attributes, $key);
}
/**
* Remove one or many items from the session.
*
* @param string|array $keys
* @return void
*/
public function forget($keys)
{
Arr::forget($this->attributes, $keys);
}
/**
* Remove all of the items from the session.
*
* @return void
*/
public function flush()
{
$this->attributes = [];
}
- 保存session
/**
* Save the session data to storage.
*
* @return bool
*/
public function save()
{
$this->ageFlashData();
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}
有兩個方法配合CookieSessionHandler
使用
/**
* Determine if the session handler needs a request.
*
* @return bool
*/
public function handlerNeedsRequest()
{
return $this->handler instanceof CookieSessionHandler;
}
/**
* Set the request on the handler instance.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function setRequestOnHandler($request)
{
if ($this->handlerNeedsRequest()) {
$this->handler->setRequest($request);
}
}
維護session
這上面過程中,並沒有使用session_set_save_handler
來設置session的管理器,session的開啓、保存什麼時候執行呢?
SessionServiceProvider
類
SessionServiceProvider
是一個服務器提供者類,session通過該類在laravel中進行註冊。
class SessionServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton(StartSession::class);
}
/**
* Register the session manager instance.
*
* @return void
*/
protected function registerSessionManager()
{
$this->app->singleton('session', function ($app) {
return new SessionManager($app);
});
}
/**
* Register the session driver instance.
*
* @return void
*/
protected function registerSessionDriver()
{
$this->app->singleton('session.store', function ($app) {
// First, we will create the session manager which is responsible for the
// creation of the various session drivers when they are needed by the
// application instance, and will resolve them on a lazy load basis.
return $app->make('session')->driver();
});
}
}
SessionServiceProvider
中以單例模式註冊了SessionManager
、Store
和StartSession
類。
StartSession
類
StartSession
類是一箇中間件,在app/Http/Kernel.php
中分配在web
中間組中。
- handle方法
handle方法是中間件重要的方法
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$this->sessionHandled = true;
// If a session driver has been configured, we will need to start the session here
// so that the data is ready for an application. Note that the Laravel sessions
// do not make use of PHP "native" sessions in any way since they are crappy.
if ($this->sessionConfigured()) {
$request->setLaravelSession(
$session = $this->startSession($request)
);
$this->collectGarbage($session);
}
$response = $next($request);
// Again, if the session has been configured we will need to close out the session
// so that the attributes may be persisted to some storage medium. We will also
// add the session identifier cookie to the application response headers now.
if ($this->sessionConfigured()) {
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
}
return $response;
}
/**
* Start the session for the given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Session\Session
*/
protected function startSession(Request $request)
{
return tap($this->getSession($request), function ($session) use ($request) {
$session->setRequestOnHandler($request);
$session->start();
});
}
/**
* Remove the garbage from the session if necessary.
*
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
protected function collectGarbage(Session $session)
{
$config = $this->manager->getSessionConfig();
// Here we will see if this request hits the garbage collection lottery by hitting
// the odds needed to perform garbage collection on any given request. If we do
// hit it, we'll call this handler to let it delete all the expired sessions.
if ($this->configHitsLottery($config)) {
$session->getHandler()->gc($this->getSessionLifetimeInSeconds());
}
}
在handle
方法中,調用startSession
方法開啓session,加載數據。調用collectGarbage
方法多過期的session數據進行垃圾回收。
terminate
方法
terminate
方法會在在響應發送到瀏覽器後自動調用。
/**
* Perform any final actions for the request lifecycle.
*
* @param \Illuminate\Http\Request $request
* @param \Symfony\Component\HttpFoundation\Response $response
* @return void
*/
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}
terminate
方法中調用Store
的save
方法把數據寫入持久化介質中。