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

简介

Laravel 是完全废弃了 PHP 官方提供的 Session 服务而自己实现了。

实现机参考文末拓展。

开始,从路由的运行说起

我们从路由调用控制器的代码来反推比较好理解!

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

//这段就是路由调用控制器的地方
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) 这个方法来获取中间件了

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

复制代码

上面的代码我们分几步来拆解:

  • 先看 $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);
   }
复制代码

这里主要看 middleware 返回值

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 属性

我们从中解析出 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

StartSession 预览

上一步可以看到在 web 请求下我们是会默认通过 StartSession 中间件的。

我们来分析 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)
    {
        $config = $this->manager->getSessionConfig();

        if ($this->configHitsLottery($config)) {
            $session->getHandler()->gc($this->getSessionLifetimeInSeconds());
        }
    }

    protected function configHitsLottery(array $config)
    {
        return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
    }

    protected function storeCurrentUrl(Request $request, $session)
    {
        if ($request->method() === 'GET' &&
            $request->route() &&
            ! $request->ajax() &&
            ! $request->prefetch()) {
            $session->setPreviousUrl($request->fullUrl());
        }
    }

    protected function addCookieToResponse(Response $response, Session $session)
    {
        if ($this->usingCookieSessions()) {
            $this->manager->driver()->save();
        }

        if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) {
            $response->headers->setCookie(new Cookie(
                $session->getName(), $session->getId(), $this->getCookieExpirationDate(),
                $config['path'], $config['domain'], $config['secure'] ?? false,
                $config['http_only'] ?? true, false, $config['same_site'] ?? null
            ));
        }
    }

    protected function getSessionLifetimeInSeconds()
    {
        return ($this->manager->getSessionConfig()['lifetime'] ?? null) * 60;
    }
    
    protected function getCookieExpirationDate()
    {
        $config = $this->manager->getSessionConfig();

        return $config['expire_on_close'] ? 0 : Carbon::now()->addMinutes($config['lifetime']);
    }

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

    protected function sessionIsPersistent(array $config = null)
    {
        $config = $config ?: $this->manager->getSessionConfig();

        return ! in_array($config['driver'], [null, 'array']);
    }

    protected function usingCookieSessions()
    {
        if ($this->sessionConfigured()) {
            return $this->manager->driver()->getHandler() instanceof CookieSessionHandler;
        }

        return false;
    }
}

复制代码

这是整个 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 这个对象,接着就是调用 setRequestOnHandlerstart() 方法

这里我们跳过 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 对象才是真正和我们打交道的对象!

用户层的体验 Illuminate\Session\Store

通过上面的分析,我么知道 Laravel 屏蔽了数据驱动层,直接向上层

提供了 Store 对象来实现对整个 Session 的调用,用户不需要在关心

底层的实现逻辑,只需要按照配置设定好驱动然后调用 Store 中提供的方法即可!

最后我们所有的 get() set() flush() 等等操作只不过是 Store 提供的服务。

拓展--实现 SessionHandlerInterface

关于实现 implements SessionHandlerInterface

其实 PHP 的针对自定义 Session 提供了预留接口,要自己拓展就必须实现这个接口中定义的方法,

PHP 底层会通过这几个方法将 Session ID 传递进来。


结语

通过本章我们要了解几个重点

  • StartSession 中间件的启动过程 ( Kernel 中配置)
  • Session 驱动的加载方式 (通过 SessionManager 工厂加载)
  • 用户最后针对 Session 的所有操作是由 Illuminate\Session\Store 对象提供
  • PHP 提供 SessionHandlerInterface 来拓展 Session 这是底层机制,必须实现。

猜你喜欢

转载自juejin.im/post/5c78c893f265da2daa31697b