【Laravel-海賊王系列】第十四章,Session 解析

簡介

Laravel 是徹底廢棄了 PHP 官方提供的 Session 服務而本身實現了。php

實現機參考文末拓展。web

開始,從路由的運行提及

咱們從路由調用控制器的代碼來反推比較好理解!redis

定位到【Laravel-海賊王系列】第十三章,路由&控制器解析的代碼api

// "這段就是路由調用控制器的地方"
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) 這個方法來獲取中間件了bash

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

複製代碼

上面的代碼咱們分幾步來拆解:cookie

  • 先看 $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);
}
複製代碼

這裏主要看 $this->middleware() 返回值session

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 屬性 app

咱們從中解析出 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\Kernelindex.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

結構預覽

上一步能夠看到在 web 請求下咱們是會默認經過 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){ ... }

    protected function configHitsLottery(array $config){ ... }

    protected function storeCurrentUrl(Request $request, $session){ ... }

    protected function addCookieToResponse(Response $response, Session $session){ ... }

    protected function getSessionLifetimeInSeconds()
    {
        return ($this->manager->getSessionConfig()['lifetime'] ?? null) * 60;
    }
    
    protected function getCookieExpirationDate(){ ... }
    
    protected function sessionConfigured()
    {
        return ! is_null($this->manager->getSessionConfig()['driver'] ?? null);
    }

    protected function sessionIsPersistent(array $config = null){ ... }

    protected function usingCookieSessions(){ ... }
}

複製代碼

這是整個 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 這個對象就是 Illuminate\Session\Store

接着就是調用 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 對象纔是真正和咱們打交道的對象!

用戶層的體驗 Store

經過上面的分析,我麼知道 Laravel 屏蔽了數據驅動層,直接向上層

提供了 Store 對象來實現對整個 Session 的調用,用戶不須要再關心

底層的實現邏輯,只須要按照配置設定好驅動而後調用 Store 中提供的方法便可!

最後咱們全部的 get() set() flush() 等等操做只不過是 Store 提供的服務。

拓展--實現 SessionHandlerInterface

關於實現 implements SessionHandlerInterface

其實 PHP 的針對自定義 Session 提供了預留接口,要本身拓展就必須實現這個接口中定義的方法,

PHP 底層會經過這幾個方法將 SessionID 傳遞進來。


結語

經過本章咱們要了解幾個重點

  • StartSession 中間件的啓動過程 ( Kernel 中配置)
  • Session 驅動的加載方式 (經過 SessionManager 工廠加載)
  • 用戶最後針對 Session 的全部操做是由 Illuminate\Session\Store 對象提供
  • PHP 提供 SessionHandlerInterface 來拓展 Session 這是底層機制,必須實現。
相關文章
相關標籤/搜索