本篇从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
方法把数据写入持久化介质中。