Laravel5.5源码详解 -- Session的启动分析

Laravel5.5源码详解 – Session的启动分析

Session的整个过程包括三个主要流程(laravel默认的sesssion名称都是laravel_session),
(1)启动session,
(2)操作session,对数据进行CRUD增删改查操作,
(3)关闭session。

Session启动之后的操作,和数据库的操作类似。这里不打算讲解。这里只关注启动过程,其一是因为session本身,这里相对比较难理解,其二是因为,CSRF等都是在session的启动过程中传入进去的,所以有必要了解。

首先看一下调用关系,session作为中间件由pipeline引入,我们从session的handle函数开始,这个函数在
\Illuminate\Session\Middleware\StartSession::class。
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;
}

其中Handle调用 startSession 再调用 getSession & setRequestOnHandler 。我们一步一步来,慢慢剥开这些外衣,

protected function startSession(Request $request)
{
    return tap($this->getSession($request), function ($session) use ($request) {
    //下面这句没细查,先跳过。我调试了下,没发现有干任何有意义的事情,可能是采用redis等时才需要,
    //这里配置的是file。与其相关的接口是SessionHandlerInterface,这个一般是php对session接口设计的,
    //如memcached, redis等。
        $session->setRequestOnHandler($request);
        //关键是后面这句
        $session->start();
    });
}

startSession()包括两步,首先是获取session的实例,也就是\Illuminate\Session\Store,主要步骤是 session= this->manager->driver(),下面随后讲解,第二步,就是通过该实例从存储介质中读取所需要的数据,相关代码在$session->start()中体现。

第一步的源码:获取session实例\Illuminate\Session\Store

在SessionStart.php里,

 public function getSession(Request $request)
{
    return tap($this->manager->driver(), function ($session) use ($request) {
        $session->setId($request->cookies->get($session->getName()));
    });
}

这个$this->manager->driver()是这么个东东?

Store {#325 ▼
id: "4qbYEQItvRNIJKSfAeRO0rGT1S4JGk3bAuedRFEJ"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#326 ▼
    #files: Filesystem {#101}
    #path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
    #minutes: "120"
  }
  started: false
}

说明白点,$this->manager->driver()就相当于StartSession->SessionManager->Store。也就是session实例本尊。这个manager,就是SessionManger类。

那么laravel的sessionManager是什么时候配置store的呢?
本质上,是调用了父类Illuminate\Support\Manager里面的driver()函数,该函数又调用Illuminate\Session\SessionManager中的getDefaultDriver()函数。我们先看getDefaultDriver()函数
public function getDefaultDriver()
{
return $this->app[‘config’][‘session.driver’];
}
这个app[‘config’]的来源,后面还会详细讲解,这理先了解结果。总之,就是把/bootstrap/cache/config.php 里面的配置项session.driver拉出来。比如我的配置项是这样的,

'session' => 
 array (
'driver' => 'file',
'lifetime' => '120',
'expire_on_close' => false,
'encrypt' => false,
'files' => 'D:\\wamp64\\www\\laravel\\larablog\\storage\\framework/sessions',   
…此处省略若干行
 )

再看driver()函数。整体上,driver()函数是下面这个样子,其中的createDriver后面还会讲到,这个又和buildStore有关。

public function driver($driver = null)
{
    //得到/bootstrap/cache/config.php 里面的配置项
    $driver = $driver ?: $this->getDefaultDriver();

    //如果这个store不存在的话,就创建一个,store实例就是在这创建的。
    if (! isset($this->drivers[$driver])) {
        $this->drivers[$driver] = $this->createDriver($driver);
    }

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

最后 this>manager>driver() this->drivers[“file”],也就是下面的store,

Store {#326 ▼
id: "cp0yC4HOkgwAq2h9FfGeLF98RM3IgscXkENFRFnp"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#327 ▼
    #files: Filesystem {#101}
    #path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
    #minutes: "120"
  }
started: false
}

说明一下,这个id没有什么实质意义,每次刷新都会不同。

那么我们明白,startSession 首先运行的就是getSession(),其中得到的是store,也就是$session。
再注意getSession()中这一小行,$session->setId($request->cookies->get($session->getName()));
$request->cookies是在request创建时就传递进去的,是一个数组,如下,

cookies: ParameterBag {#21 ▼
    #parameters: array:2 [▼
    "XSRF-TOKEN" => "eyJpdiI6IllOSStvZlMwNEk0T084eWJVWFZ5VVE9PSI▶"
    "laravel_session" => "eyJpdiI6IlRNSCtGcmlkeEEybGc5TzUzdXRyR ▶"
    ]
 }

这里要说一句,就是读者不必去关注XSRF-TOKEN的具体值,因为调试时,每次都会不一样,在实际运行时,只有一个。

接上面,$session->getName()就是那个名称”laravel_session”,经过$session->setId()处理之后,会得到这么一个store,

Store {#325 ▼
id: "0z8Sd3lsnJYIkTBILQeamqh80floYcF0InWwdgm2"
name: "laravel_session"
attributes: []
handler: FileSessionHandler {#326 ▼
    #files: Filesystem {#101}
    #path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
    #minutes: "120"
  }
started: false
}

还是前面提到的那个store,也是getSession的返回值。

第二步,读取session数据

startSession在得到$session(也就是store)之后,接下来会启动下面这个start,现在来看Session->start()

public function start()
{
    //最重要的一条语句,实质是读取保存的session数据
    $this->loadSession();
    //如果没有CSRF_TOKEN,就创建一个
    if (! $this->has('_token')) {
            $this->regenerateToken();
    }
    //session启动完毕(就是取到了需要的数据而已),返回
    return $this->started = true;
}

protected function loadSession()
{
    $this->attributes = array_merge($this->attributes, $this->readFromHandler());
}

protected function readFromHandler()
{   
    //read读到session中的数据
    if ($data = $this->handler->read($this->getId())) {
        //反序列化 -- 从已存储的session数据中创建 PHP 的值。
        $data = @unserialize($this->prepareForUnserialize($data));
        //确定读到的数据是非空数组。 
        if ($data !== false && ! is_null($data) && is_array($data)) {
            return $data;
        }
    }

    return [];
}

在上面的函数中,readFromHandler里用到的这个handler是指向下面这个我们用来存储session数据的具体位置,

FileSessionHandler {#326 ▼
files: Filesystem {#101}
path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
minutes: "120"
}

必须特别说明的是,这里我用的是laravel默认的file,也就是文件来存储session。所以,handler也就是FileSessionHandler。如果你的配置不一样,那么handler就会不一样。
继续深入readFromhandler之前,还要了解以下两点:store的创建和参数传递,及FileSessionHandler。

Store是什么时候被创建的?如何传递参数。

现在回过头来看前面提到的createDriver了,前面已经提到过,该函数被Illuminate\Support\Manager里面的driver()函数调用。其原型是这样子的,

    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.");
    }

这里面的$method,如果你打印出来的话,正是”createFileDriver”。所以,最终创建store的命令,也就是下面要提到的createFileDriver。这也是因为我们使用的是file作为store配置的结果,所以看这个

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
   ));
}

protected function buildSession($handler)
{
    if ($this->app['config']['session.encrypt']) {
        return $this->buildEncryptedSession($handler);
    } else {
        //默认的情况下是不加密的session,所以执行下面这句,
        return new Store($this->app['config']['session.cookie'], $handler);
    }
}

最后这里createFileDriver调用了createNativeDriver,其中又用new createNativeDriver创建了这个handler作为参数,传递给buildSession ,buildSession再用这个handler创建了store。最重要的,是要了解new Store($this->app['config']['session.cookie'], $handler)这一句,Store也正是在这里被创建的。

了解FileSessionHandler

前面已经特别强调,那个反复提到的handler是FileSessionHandler,他实现了SessionHandlerInterface这个PHP接口。
回想起前面提到的readFromHandler这个函数,其中最关键的就是获取session数据这一句:$data = $this->handler->read($this->getId()),这个read,也正是调用了FileSessionHandler里的read函数

public function read($sessionId)
{
    if ($this->files->exists($path = $this->path.'/'.$sessionId)) {
        if (filemtime($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
            return $this->files->get($path, true);
        }
    }

    return '';
}

把那个$path打印出来会是下面这个样子,这也是数据存储的文件名(是不是觉得名字好怪?)。对照看一下,

"D:\wamp64\www\laravel\larablog\storage\framework/sessions/0z8Sd3lsnJYIkTBILQeamqh80floYcF0InWwdgm2"

该read函数最后返回的$this->files->get($path, true)正是文件里的内容,其中包括那个大名鼎鼎的CSRF-TOKEN。

a:4:{
s:6:"_token";s:40:"zalGw4lffAeW6alsQKFBxb54QE9o3hIpEb0kK3Sd";
s:9:"_previous";
a:1:{s:3:"url";s:33:"http://localhost:8000/admin/login";}
s:6:"_flash";
a:2:{ s:3:"old";a:0:{}s:3:"new";a:0:{} }
s:22:"PHPDEBUGBAR_STACK_DATA";
a:0:{}
}

这个数据再经过前面提到的readFromhandler的反序列化unserialize操作,会是下面这个样子

array:4 [▼
  "_token" => "zalGw4lffAeW6alsQKFBxb54QE9o3hIpEb0kK3Sd"
  "_previous" => array:1 [▼
    "url" => "http://localhost:8000/admin/login"
  ]
  "_flash" => array:2 [▼
    "old" => []
    "new" => []
  ]
  "PHPDEBUGBAR_STACK_DATA" => []
]

到这里,极为值得一提的是那个_token,你是不是在程序调试CSRF_TOKEN时经常碰到TokenMismatchException的问题?到这里查一下,说不定有意想不以的收获!
另外,关于这个CSRF-TOKEN,我们看到有两个地方,一个是在这里直接引入,如果这里没有,就会在Session->start()中用regenerateToken()创建。

至此,startSession完全运行完毕。
但还有些疑问,比如session是什么时候配置的?

在前面讲到的handle 函数中,在startSession之前,有这么一句配置函数
if ($this->sessionConfigured()) 。。。
这个sessionConfigured是这样的,

protected function sessionConfigured()
{
    return ! is_null($this->manager->getSessionConfig()['driver'] ?? null);
}

该函数再调用sessionManager的getSessionConfig()来寻找配置,

直接dump($this)把sessionManager打印出来,看看这到底是个什么东西?(代码很长,所有有省略)

SessionManager {#193 ▼
  app: Application {#2 ▶}
  customCreators: []
  drivers: array:1 [▼
    file" => Store {#490 ▼
      #id: "8bfH3MIt3iY5BiNUae14sCRd6QT1ScY2QwHdbBSE"
      #name: "laravel_session"
      #attributes: []
      #handler: FileSessionHandler {#586 ▼
        #files: Filesystem {#101}
        #path: "D:\wamp64\www\laravel\larablog\storage\framework/sessions"
        #minutes: "120"
      }
      #started: false
    }
  ]
}

在sessionManger中加入调试代码,

public function getSessionConfig()
{
    dump($this);
    dd();
    return $this->app['config']['session'];
}

得到下面的结果,

$this就是SessionManager,

    SessionManager {#193 ▼
    app: Application {#2#basePath: "D:\wamp64\www\laravel\larablog"}
    customCreators: []
    drivers: []
    }

$this->app[‘config’]是下面这个东西,

Repository {#24 ▼
  items: array:14 [▼
    "app" => array:13 [▶]
    "auth" => array:4 [▶]
    "broadcasting" => array:2 [▶]
    "cache" => array:3 [▶]
    "database" => array:4 [▶]
    "debugbar" => array:13 [▶]
    "filesystems" => array:3 [▶]
    "mail" => array:9 [▶]
    "queue" => array:3 [▶]
    "scout" => array:5 [▶]
    "services" => array:4 [▶]
    "session" => array:15 [▶]
    "view" => array:2 [▶]
    "trustedproxy" => array:2 [▶]
  ]
}

要明白那个$this->app[‘config’]是如何得到Repository的,看下面:config 配置文件的加载。

插曲:config 配置文件的加载

config 配置文件由类 \Illuminate\Foundation\Bootstrap\LoadConfiguration::class 完成:
public function bootstrap(Application $app)

{
    $items = [];
    if (file_exists($cached = $app->getCachedConfigPath())) {
        //注意,这个$cached返回的是路径,
        //"D:\wamp64\www\laravel\larablog\bootstrap/cache/config.php"
        $items = require $cached;
        //items就相当于/bootstrap/cache/config.php里面的所有配置项
        $loadedFromCache = true;
    }
    //下面,new Repository($items)创建一个仓库,并把所有配置项作为参数传递进去,
    //然后绑定到$app->instances数组中的config上,说明config已经实例化。
    $app->instance('config', $config = new Repository($items));
    //此时,如果打印出$app,就会得到后面的那些内容,可以明显看到,
    //instances数组中添加了’config’ (已经刻意展开了该项)
    if (! isset($loadedFromCache)) {
        $this->loadConfigurationFiles($app, $config);
    }

   $app->detectEnvironment(function () use ($config) {
        return $config->get('app.env', 'production');
    });

    date_default_timezone_set($config->get('app.timezone', 'UTC'));

    mb_internal_encoding('UTF-8');
}

dd(#app)得到的结果,

Application {#2 ▼
basePath: "D:\wamp64\www\laravel\larablog"
  …
instances: array:17 [▼
…
"config" => Repository {#24 ▼
  #items: array:14 [▼
    "app" => array:13 [▶]
    "auth" => array:4 [▶]
    "broadcasting" => array:2 [▶]
    "cache" => array:3 [▶]
    "database" => array:4 [▶]
    "debugbar" => array:13 [▶]
    "filesystems" => array:3 [▶]
    "mail" => array:9 [▶]
    "queue" => array:3 [▶]
    "scout" => array:5 [▶]
    "services" => array:4 [▶]
    "session" => array:15 [▶]
    "view" => array:2 [▶]
    "trustedproxy" => array:2 [▶]
  ]
  …
}

这个配置,也就是前面的getDefaultDriver()中拉出来用的配置。
当然,有兴趣的朋友可以继续研究loadConfigurationFiles。

至此,我们已经 全面了解这个session是如何启动的了。

posted @ 2017-12-14 21:21  SpaceVision  阅读(34)  评论(0编辑  收藏  举报