Code Monkey home page Code Monkey logo

laravel's Issues

01. HTTP 入口解析

HTTP 入口解析

public/index.php 入口

public/index.php 中第 24 行

require __DIR__.'/../vendor/autoload.php';

逻辑为,引入 ../vendor/autoload.php,即 composer 产生的 vendor/autoload.php

接着第 38 行

$app = require_once __DIR__.'/../bootstrap/app.php';

这里面做的事情不少,具体的我们看 bootstrap/app.php 的代码。

bootstrap/app.php 实例化容器

在第 [14-16行]

$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);

这是 Laravel 框架内为数不多的直接 new 出来的对象,也就是框架的应用对象,
此对象继承于 Illuminate\Container\Container (容器),
所以, Laravel 框架中的容器对象一般就是指的 Illuminate\Foundation\Application 对象(因在实际运行过程中,不会直接 new Illuminate\Container\Container 的)。

在第 [29-42行]

$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);

这里的singleton是 public/index.php 中,用 Illuminate\Contracts\Http\Kernel 能注入后取出 App\Http\Kernel 的关键。

关于 singleton() 的解析,请见 10. 容器的 singleton 和 bind 的实现

第 55行

return $app;

在前面 public/index.php 中的

$app = require_once __DIR__.'/../bootstrap/app.php';
拿到了 return$app

public/index.php 构建 + 处理

public/index.php 第 52 行

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

这里调用了 Illuminate\Foundation\Application 的 make 方法(具体解析请见本篇结尾的链接),获得了 http 处理器 ( $kernel )

然后第 54-56 行

laravel/public/index.php

Lines 54 to 56 in 7028b17

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

先用 Illuminate\Http\Request::capture() 抓出一个 Illuminate\Http\Request HTTP请求对象

Request::capture 请求捕获

Illuminate\Http\Request::capture() 的代码为

public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}

其中调用了 Symfony\Component\HttpFoundation\Request::createFromGlobals()

public static function createFromGlobals()
{
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
) {
parse_str($request->getContent(), $data);
$request->request = new ParameterBag($data);
}
return $request;
}

其作用是讲 HTTP 请求过来的参数,按照 key => value 格式生成 Symfony\Component\HttpFoundation\ParameterBag 对象。
然后将包含 ParameterBag$request 对象交给 Illuminate\Http\Request::createFromBase()
public static function createFromBase(SymfonyRequest $request)
{
if ($request instanceof static) {
return $request;
}
$content = $request->content;
$request = (new static)->duplicate(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), $request->files->all(), $request->server->all()
);
$request->content = $content;
$request->request = $request->getInputSource();
return $request;
}

(new static)->duplicate() 最终执行了

public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
{
$dup = clone $this;
if (null !== $query) {
$dup->query = new ParameterBag($query);
}
if (null !== $request) {
$dup->request = new ParameterBag($request);
}
if (null !== $attributes) {
$dup->attributes = new ParameterBag($attributes);
}
if (null !== $cookies) {
$dup->cookies = new ParameterBag($cookies);
}
if (null !== $files) {
$dup->files = new FileBag($files);
}
if (null !== $server) {
$dup->server = new ServerBag($server);
$dup->headers = new HeaderBag($dup->server->getHeaders());
}
$dup->languages = null;
$dup->charsets = null;
$dup->encodings = null;
$dup->acceptableContentTypes = null;
$dup->pathInfo = null;
$dup->requestUri = null;
$dup->baseUrl = null;
$dup->basePath = null;
$dup->method = null;
$dup->format = null;
if (!$dup->get('_format') && $this->get('_format')) {
$dup->attributes->set('_format', $this->get('_format'));
}
if (!$dup->getRequestFormat(null)) {
$dup->setRequestFormat($this->getRequestFormat(null));
}
return $dup;
}

其将重要的 http 请求头、请求参数、请求文件、cookie 等信息赋给相应属性。

在后面的

$request->request = $request->getInputSource();

Request::getInputSource() 的代码是
protected function getInputSource()
{
if ($this->isJson()) {
return $this->json();
}
return in_array($this->getRealMethod(), ['GET', 'HEAD']) ? $this->query : $this->request;
}

作用是调用 $request->input() 之时

GET、HEAD 请求,能取到 GET 参数,
POST 请求,能取到 POST 参数。

至此,Request::capture() 就执行完了。

public/index.php 运行逻辑

然后交给 App\Http\Kernelhandle 方法进行处理,获得 $response (Illuminate\Http\Request 对象)。

Laravel 框架的设计绝大可扩展部分(如服务提供者、中间件)都是在这个阶段处理和触发的,
所以我单独把 02. HTTP Kernel Handle 解析 拆出去了。

最后,在第 58-60行

laravel/public/index.php

Lines 58 to 60 in 7028b17

$response->send();
$kernel->terminate($request, $response);

调用 Illuminate\Http\Response 的 send 方法,将响应的状态码/头/内容返回给客户端(浏览器),具体过程可见本篇末尾的引用。


16. UserProvider、Guard 和 Authenticatable 机制

Laravel 提供了完善的用户状态判断鉴定、验证机制,这篇文章我们用十五分钟左右的时间来深入框架内部来了解它。
在学习之前,建议先查阅 Laravel China 文档库 > Laravel 5.7 > 用户认证,熟悉用法后,学习效率更高。

相关类

描述
Illuminate\Auth\SessionGuard guard 定义为 web 生效
Illuminate\Auth\EloquentUserProvider 默认的用户提供者
App\Http\Middleware\Authenticate 中间件
App\User 应用自定义 User 模型
Illuminate\Auth\Authenticatable 这是一个 trait,被 App\User 的父类 Illuminate\Foundation\Auth\User 所使用

配置文件解析

Auth::guard 可用的守护者定义在:

laravel/config/auth.php

Lines 38 to 48 in 4d76a68

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],

这些配置文件中的 providers 定义在

laravel/config/auth.php

Lines 67 to 77 in 4d76a68

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],


'driver' => 'session',
'driver' => 'token',

sessiontoken 分别指的是 Illuminate\Auth\SessionGuardIlluminate\Auth\TokenGuard

具体的构建方法见 AuthManager::createSessionDriverAuthManager::createTokenDriver,这两个方法是在 AuthManager::resolve 时用以下代码调用的。

$driverMethod = 'create'.ucfirst($config['driver']).'Driver';

注册流程 基础 + 容器部分

Illuminate\Auth\AuthManager

class AuthManager implements FactoryContract
{
use CreatesUserProviders;

用到了 Illuminate\Auth\CreatesUserProviders trait
此 trait 提供如下方法

  • public createUserProvider($provider = null): Illuminate\Contracts\Auth\UserProvider|null
  • protected getProviderConfiguration($provider): array|null
  • protected createDatabaseProvider($config): Illuminate\Auth\DatabaseUserProvider
  • protected createEloquentProvider($config): Illuminate\Auth\EloquentUserProvider
  • public getDefaultUserProvider(): string

Illuminate\Foundation\ApplicationregisterCoreContainerAliases 方法中,注册 auth 时, Illuminate\Auth\AuthManager 被使用。

public function registerCoreContainerAliases()
{
foreach ([
'app' => [\Illuminate\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class],
'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class],
'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class],
'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class],
'db' => [\Illuminate\Database\DatabaseManager::class],
'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
'files' => [\Illuminate\Filesystem\Filesystem::class],
'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class],
'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class],
'hash' => [\Illuminate\Hashing\HashManager::class],
'hash.driver' => [\Illuminate\Contracts\Hashing\Hasher::class],
'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class],
'log' => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class],
'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class],
'auth.password' => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class],
'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class],
'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class],
'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
'redirect' => [\Illuminate\Routing\Redirector::class],
'redis' => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class],
'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
'session' => [\Illuminate\Session\SessionManager::class],
'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class],
'url' => [\Illuminate\Routing\UrlGenerator::class, \Illuminate\Contracts\Routing\UrlGenerator::class],
'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class],
'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class],
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}

容器对象的 registerCoreContainerAliases 方法是在 __construct 阶段被触发的。

注册流程 服务提供者 Illuminate\Auth\AuthServiceProvider

Illuminate\Auth\AuthServiceProvider 中的 register 方法,

public function register()
{
$this->registerAuthenticator();
$this->registerUserResolver();
$this->registerAccessGate();
$this->registerRequestRebindHandler();
}

registerAuthenticator 方法为

protected function registerAuthenticator()
{
$this->app->singleton('auth', function ($app) {
// Once the authentication service has actually been requested by the developer
// we will set a variable in the application indicating such. This helps us
// know that we need to set any queued cookies in the after event later.
$app['auth.loaded'] = true;
return new AuthManager($app);
});
$this->app->singleton('auth.driver', function ($app) {
return $app['auth']->guard();
});
}

registerUserResolver 方法为

protected function registerUserResolver()
{
$this->app->bind(
AuthenticatableContract::class, function ($app) {
return call_user_func($app['auth']->userResolver());
}
);
}

这里便将 authIlluminate\Contracts\Auth\Authenticatable 成功的从 Application 中的 alias 升级为 singleton 。

在我们的 config/app.phpproviders 第一个便是 Illuminate\Auth\AuthServiceProvider

laravel/config/app.php

Lines 122 to 127 in 4d76a68

'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,

到这里,web 环境框架启动部分所有初始化用户状态维护机制就完成。

中间件流程 限制登陆后可访问

当一个路由声明必须登陆后可访问,也就是 auth 中间件,会触发:

public function __construct(Auth $auth)
{
$this->auth = $auth;
}

触发容器构建 AuthManager

在容器构建 AuthManager 时,会触发 __construct

public function __construct($app)
{
$this->app = $app;
$this->userResolver = function ($guard = null) {
return $this->guard($guard)->user();
};
}

中间件的 handle

public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}

调用了中间件 authenticate 方法

protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}

如果没登陆,报错

throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);

获取当前用户流程

如果手工在业务中使用 auth()->user() 会触发

public function __call($method, $parameters)
{
return $this->guard()->{$method}(...$parameters);
}

AuthManagerguard() 默认返回的是 SessionGuard,所以 auth()->user() 调用的是

public function user()
{
if ($this->loggedOut) {
return;
}
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$recaller = $this->recaller();
if (is_null($this->user) && ! is_null($recaller)) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}

其 131 行的 $this->provider->retrieveById($id) 调用了 EloquentUserProvider::retrieveById

public function retrieveById($identifier)
{
$model = $this->createModel();
return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->first();
}

$this->createModel() 返回的是 $this->model,是在

public function __construct(HasherContract $hasher, $model)
{
$this->model = $model;
$this->hasher = $hasher;
}
设置进去的。

AuthManager 调用 createUserProvider 后调用 createEloquentProvider 方法时,

public function createUserProvider($provider = null)
{
if (is_null($config = $this->getProviderConfiguration($provider))) {
return;
}
if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
return call_user_func(
$this->customProviderCreators[$driver], $this->app, $config
);
}
switch ($driver) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
);
}

protected function createEloquentProvider($config)
{
return new EloquentUserProvider($this->app['hash'], $config['model']);
}

其传进去的 $config 的获取逻辑为

protected function getProviderConfiguration($provider)
{
if ($provider = $provider ?: $this->getDefaultUserProvider()) {
return $this->app['config']['auth.providers.'.$provider];
}
}

也就是这里设定的 'model' => App\User::class

laravel/config/auth.php

Lines 67 to 77 in 4d76a68

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],

前面 getAuthIdentifierName 的方法,就是调用 App\User::getAuthIdentifierName 来查找用户了。

return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->first();


本文章采用「署名 4.0 国际」创作共享协议,
转载前请阅读 相关说明 »

11. 事件机制

根据 Laravel 的 Events 文档 (中文版:事件系统), EventServiceProvider 的使用方法为:

/**
 * 注册应用中的其它事件。
 *
 * @return void
 */
public function boot()
{
    parent::boot();

    Event::listen('event.name', function ($foo, $bar) {
        //
    });
}

如果花点时间研究 Event 这个 Facade,我们可以找到他穿透类其实为 Illuminate\Events\Dispatcher

Illuminate\Events\Dispatcher 类的 __construct 方法为:

public function __construct(ContainerContract $container = null)
{
$this->container = $container ?: new Container;
}

作用是将容器设置到 $this->container 属性

监听

我们刚刚在 EventServiceProvider 调用的 Event::listen 方法的代码为:

public function listen($events, $listener)
{
foreach ((array) $events as $event) {
if (Str::contains($event, '*')) {
$this->setupWildcardListen($event, $listener);
} else {
$this->listeners[$event][] = $this->makeListener($listener);
}
}
}

如果监听了通配事件,setupWildcardListen 在辗转后跟 else 一样的调用到了 makeListener

public function makeListener($listener, $wildcard = false)
{
if (is_string($listener)) {
return $this->createClassListener($listener, $wildcard);
}
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return $listener($event, $payload);
}
return $listener(...array_values($payload));
};
}

注意,返回的是闭包!并不会立即执行。

在存放到 $this->listeners 后,事件们就静静地等待被触发

$this->listeners[$event][] = $this->makeListener($listener);

触发

在我们调用 event() 辅助方法后,触发的逻辑其实走到了 dispatch 方法

public function dispatch($event, $payload = [], $halt = false)
{
// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
list($event, $payload) = $this->parseEventAndPayload(
$event, $payload
);
if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}
$responses = [];
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && ! is_null($response)) {
return $response;
}
// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}

核心逻辑就这句:

$response = $listener($event, $payload);

13. Macroable 解析

还记得那个《高速修车》的陈年老梗吗:

公司的业务呢,就像跑在高速路上的车,车不能停,但是新需求和修bug也不能停。

这里要跟大家扒代码的 Macroable 呢,有点像上面这个悖论的出路;但其实也不是,因为他没有实现 且不停,只实现了 且不停。请原谅我这尴尬的幽默。


从陈年教程入手

网上很多教程都是提及了,Laravel 绝大部分类,都可以扩展方法。比如这个例子:

use Illuminate\Support\Collection;

Collection::macro('bcsum', function () {
    $sum = 0;
    foreach ($this as $item) {
        $sum = bcadd($sum, $item, 2);
    }
    return $sum;
});

dump((new Collection([1,2,3,4]))->bcsum()); // it says "10.00"

我们可以看到,在不改变代码的前提下,我们在使用的过程中给 Illuminate\Support\Collection 扩充了 bcsum 的功能。就如同在其中加入了如下代码:

<?php

class Collection ...
{
    public function bcsum()
    {
        $sum = 0;
        foreach ($this as $item) {
            $sum = bcadd($sum, $item, 2);
        }
        return $sum;
    }
}

代码解析

class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
use Macroable;

我们看到,Illuminate\Support\Collection 使用了 Illuminate\Support\Traits\Macroable 这个 trait

macro 的代码

作用是将闭包存进static::$macros

/**
* Register a custom macro.
*
* @param string $name
* @param object|callable $macro
*
* @return void
*/
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}

魔术方法

在调用我们的方法时,按名字匹配出来,参数传入触发。

/**
* Dynamically handle calls to the class.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public static function __callStatic($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $parameters);
}
/**
* Dynamically handle calls to the class.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}

这里之所以定义了两个方法,__call__callStatic,是因为 laravel 很多类,部分可以静态调用,部分可以动态调用。macroable 如果要能被这两种类可用,那么就需要定义两个魔术方法。


经验分享

失灵的情况

一. 魔术一个魔术方法,不可行

我们现在知道了 Macroable 的神奇,但是其实Macroable 也有失灵的时候。举个例子:

<?php
class JoeDoe
{
    use \Illuminate\Support\Traits\Macroable;
}

JoeDoe::macro('__toString', function () {
    return '123';
});

echo new JoeDoe();

而运行时,其还是报了这个错:

PHP Recoverable fatal error:  Object of class JoeDoe could not be converted to string

但是如果我们在 JoeDoe 这个类中手工硬编码 __toString 这个方法却是可以运行的的;
为什么?

其实道理很简单,因为 echo JoeDoe 对象时候,php 内核检查其中有没有 __toString 这个方法时,就已经报错了,根本还来不及走到 __call 这个魔术方法。

12. Facade 机制

总结: Facade 就是一个标记便捷的调用容器盛放对象的方法的设计。

一般的,Facade 类都继承于 Illuminate\Support\Facades\Facade

比如 Illuminate\Support\Facades\Gate 这个门面:

<?php
namespace Illuminate\Support\Facades;

use Illuminate\Contracts\Auth\Access\Gate as GateContract;

class Gate extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return GateContract::class;
    }
}

代码中我们只需要留意 getFacadeAccessor 方法是在 Illuminate\Support\Facades\Gate 中定义的就好了。
我们来分析 Illuminate\Support\Facades\Facade

魔术方法 part 1

首先我们来看末尾的 __callStatic 方法:

/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}

还记得 Facade 类我们是怎么调用的吗?

静态调用!比如 Gate::allows()

因为在 Facade 类中定义了 __callStatic 魔术方法,所以只要静态调用了里面没有提供的方法,都会走到魔术方法里面。
这个魔术方法内部有一个取出实例的逻辑:

$instance = static::getFacadeRoot();

代码位于

/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}

前面,我们留意了 static::getFacadeAccessor() 是位于 Illuminate\Support\Facades\Gate 中的,而 static::getFacadeAccessor() 运行后作为参数给了 static::resolveFacadeInstance(),后者实现为

/**
* Resolve the facade root instance from the container.
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}

static::$app 就是容器对象

/**
* The application instance being facaded.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected static $app;

这句

return static::$resolvedInstance[$name] = static::$app[$name];

尝试从容器对象 数组取 操作,就是调用了 make
public function offsetGet($key)
{
return $this->make($key);
}

就是从容器取出对象。

总结 getFacadeRoot 的逻辑大概为

  1. 判断 static::getFacadeAccessor() 返回是否对象,如果对象则直接使用。
  2. Facade::$resolvedInstance 数组中尝试根据 $name 取出对象,避免多次调用时,每次都取。
  3. Illuminate\Foundation\Application 容器取出对象。

魔术方法 part 2

拿到 $instance 后,先验证 $instance 是否为空。如果是就报错 A facade root has not been set.

if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}

所以如果遇到 A facade root has not been set. 这个错误,一般都是 a. 相应的服务提供者未注册; b. 对象绑定到容器的别名跟 static::getFacadeAccessor() 返回不一致。

最后,直接把方法和参数穿透到背后的 $instance 去执行。

Laravel 有很多 facade 门面,背后穿透的不只一个类(譬如 DBViewSession),这是怎么回事呢?
请看 15. Laravel 神奇的 Manager 类

02. HTTP Kernel Handle 解析

Kernel Handle

App\Http\Kernel 继承自 Illuminate\Foundation\Http\Kernel 类,所以本文章的分析主要集中在 app/Http/Kernel.phpvendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php 两个类中

__construct 解析

因为 App\Http\Kernel 没有 __construct 方法,所以穿透到了 Illuminate\Foundation\Http\Kernel 的 __construct:

/**
* Create a new HTTP kernel instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;
foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}

首先是此方法传入参数的 __construct(Application $app, Router $router) 声明,这里恰好使用了 Laravel 容器的依赖注入(Dependency Injection,Inversion of Control的一种)设计。具体的在本篇不讲述,可参见本篇末尾的单独分析的链接的文章。

执行到 __construct 时, Illuminate\Foundation\Application 容器会将 Illuminate\Foundation\Application(即应用容器自身)和 Illuminate\Routing\Router 注入到方法内。然后逻辑代码将两个对象赋给 App\Http\Kernel$this 属性中。

然后将 Illuminate\Foundation\Http\Kernel 的 $middlewarePriority 属性

/**
* The priority-sorted list of middleware.
*
* Forces the listed middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];

赋值给 Illuminate\Routing\Router 的 $middlewarePriority 属性

根据其注释,此属性是强制对 middleward 中间件执行顺序进行排序的作用。

在后面将 App\Http\Kernel 中声明的 $middlewareGroup

/**
* The application's route middleware groups.
*
* @var array
*/
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',
],
];

Illuminate\Foundation\Http\Kernel 的 96-98行,调用 Illuminate\Routing\Router 的 middlewareGroup 方法,存到 $router 的 $middlewareGroup 中
/**
* Register a group of middleware.
*
* @param string $name
* @param array $middleware
* @return $this
*/
public function middlewareGroup($name, array $middleware)
{
$this->middlewareGroups[$name] = $middleware;
return $this;
}

紧接着,将

/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
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\Routing\Router 的 aliasMiddleware 的方法,绑定到 $router 的 $middleware 中
/**
* Register a short-hand name for a middleware.
*
* @param string $name
* @param string $class
* @return $this
*/
public function aliasMiddleware($name, $class)
{
$this->middleware[$name] = $class;
return $this;
}

至此, Kernel::__construct() 解析完毕。

handle 解析

/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new Events\RequestHandled($request, $response)
);
return $response;
}

调用到 Symfony\Component\HttpFoundation\Request 的 enableHttpMethodParameterOverride 方法

Illuminate\Http\Request 继承自 Symfony\Component\HttpFoundation\Request
并且 Illuminate\Http\Request 未覆盖 enableHttpMethodParameterOverride

/**
* Enables support for the _method request parameter to determine the intended HTTP method.
*
* Be warned that enabling this feature might lead to CSRF issues in your code.
* Check that you are using CSRF tokens when required.
* If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
* and used to send a "PUT" or "DELETE" request via the _method request parameter.
* If these methods are not protected against CSRF, this presents a possible vulnerability.
*
* The HTTP method can only be overridden when the real HTTP method is POST.
*/
public static function enableHttpMethodParameterOverride()
{
self::$httpMethodParameterOverride = true;
}

然后,调用 Illuminate\Foundation\Http\Kernel 的 sendRequestThroughRouter

/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

通过第142行,将 $request 注入进 Illuminate\Foundation\Application 容器。

144行,将门面类中的 request 数据清理掉

/**
* Clear a resolved facade instance.
*
* @param string $name
* @return void
*/
public static function clearResolvedInstance($name)
{
unset(static::$resolvedInstance[$name]);
}

Bootstrap 解析

然后146行,调用 Illuminate\Foundation\Http\Kernel 的 bootstrap 方法

/**
* Bootstrap the application for HTTP requests.
*
* @return void
*/
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}

第161执行到容器的 hasBeenBootstrapped 方法

/**
* Determine if the application has been bootstrapped before.
*
* @return bool
*/
public function hasBeenBootstrapped()
{
return $this->hasBeenBootstrapped;
}

第162实际得到这个数组

/**
* The bootstrap classes for the application.
*
* @var array
*/
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];

然后将数组做为参数,执行容器的 bootstrapWith 方法
/**
* Run the given array of bootstrap classes.
*
* @param array $bootstrappers
* @return void
*/
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}

特别留意206行的 make 调用

关于容器 make 方法的细节
请查阅 10. 容器的 singleton 和 bind 的实现 的 “揭开 Container::make() 神秘的面纱” 段落

/**
* Resolve the given type from the container.
*
* (Overriding Container::make)
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract, $parameters);
}

关于 loadDeferredProvider 的逻辑会最终执行到
/**
* Register a deferred provider and service.
*
* @param string $provider
* @param string|null $service
* @return void
*/
public function registerDeferredProvider($provider, $service = null)
{
// Once the provider that provides the deferred service has been registered we
// will remove it from our local list of the deferred services with related
// providers so that this container does not try to resolve it out again.
if ($service) {
unset($this->deferredServices[$service]);
}
$this->register($instance = new $provider($this));
if (! $this->booted) {
$this->booting(function () use ($instance) {
$this->bootProvider($instance);
});
}
}

第710行的 booting 方法,是登记一个闭包 (并不会马上执行这个闭包), 然后这个服务提供者在 boot() 阶段的 $this->fireAppCallbacks($this->bootingCallbacks) 才会真正被创建。 关联阅读请见 04. ServiceProvider Boot 解析

然后结果就是依次执行

  • \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables
  • \Illuminate\Foundation\Bootstrap\LoadConfiguration
  • \Illuminate\Foundation\Bootstrap\HandleExceptions
  • \Illuminate\Foundation\Bootstrap\RegisterFacades
  • \Illuminate\Foundation\Bootstrap\RegisterProviders
  • \Illuminate\Foundation\Bootstrap\BootProviders

这些 Bootstrap 类的 bootstrap 方法

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
if ($app->configurationIsCached()) {
return;
}
$this->checkForSpecificEnvironmentFile($app);
try {
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
} catch (InvalidPathException $e) {
//
} catch (InvalidFileException $e) {
echo 'The environment file is invalid: '.$e->getMessage();
die(1);
}
}

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$items = [];
// First we will see if we have a cache configuration file. If we do, we'll load
// the configuration items from that file so that it is very quick. Otherwise
// we will need to spin through every configuration file and load them all.
if (file_exists($cached = $app->getCachedConfigPath())) {
$items = require $cached;
$loadedFromCache = true;
}
// Next we will spin through all of the configuration files in the configuration
// directory and load each one into the repository. This will make all of the
// options available to the developer for use in various parts of this app.
$app->instance('config', $config = new Repository($items));
if (! isset($loadedFromCache)) {
$this->loadConfigurationFiles($app, $config);
}
// Finally, we will set the application's environment based on the configuration
// values that were loaded. We will pass a callback which will be used to get
// the environment in a web context where an "--env" switch is not present.
$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');
}

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$this->app = $app;
error_reporting(-1);
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}

/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$app->boot();
}

上面列出的清单中,分别作用为

作用
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables 加载环境变量
\Illuminate\Foundation\Bootstrap\LoadConfiguration 加载config
\Illuminate\Foundation\Bootstrap\HandleExceptions 错误处理者
\Illuminate\Foundation\Bootstrap\RegisterFacades 注册门面类
\Illuminate\Foundation\Bootstrap\RegisterProviders 注册服务提供者
\Illuminate\Foundation\Bootstrap\BootProviders 启动服务提供者

其中最后两项在 laravel 请求的生命周期中是至关重要的,我们将在后续文章中重点讲解。

bootstrap 阶段结束后,Kernel::sendRequestThroughRouter 后面带 pipeline 关键字的代码就是管道。

关于管道请查阅 05. Pipeline 解析

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());

在进入管道前, 调用了 dispatchToRouter 返回一个闭包对象

/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}

匹配路由的逻辑清晰可见

/**
* Dispatch the request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
/**
* Dispatch the request to a route and return the response.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
/**
* Find the route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
/**
* Return the response for the given route.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\Route $route
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$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()
);
});
}

如果一路抽丝剥茧,我们便能找到 Router 调用 controller 的逻辑了。 请见06. RouteServiceProvider 详解 最后段落。

09. 容器的依赖注入机制

本章节以前面没有分析完成的 02. HTTP Kernel Handle 解析06. RouteServiceProvider 详解 做铺垫,通过讲述其最后 runController 的内部逻辑,抛砖引玉,分析 Laravel 依赖注入的原理,

因为前面我们讲了容器对象的绑定的实现 (见:10. 容器的 singleton 和 bind 的实现),所以在这里不再详述。

定义

在大型项目中,因为类的繁多,彼此需要调用到的代码的更多,如果我们在用到对象的每一个地方去 new FooClass(... $parameters ),那么我们的项目重复代码太多,早晚会失去优雅性和可维护性。

所以依赖注入就是在用到一个对象的方法入参声明,你要什么对象,他基于什么实现。框架 (亦或独立的 容器服务 )在执行到这里的时候,发现你声明了需要那几个对象,在 容器服务 里找到这几个对象的实例化逻辑,实例化出来扔进去执行。

路由执行中的依赖注入的实现

解析反射

/**
* Run the route action and return the response.
*
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}

这里的 Route 对象本身已经是单个路由了,所以 $this->getController()$this->getControllerMethod() 取得的具体的 Controller 类名和方法名。

核心代码其实就是这个 $this->controllerDispatcher()->dispatch() 方法了

/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return mixed
*/
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));
}

第一步 解析参数

$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);

resolveClassMethodDependencies 的实现

/**
* Resolve the object method's type-hinted dependencies.
*
* @param array $parameters
* @param object $instance
* @param string $method
* @return array
*/
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
if (! method_exists($instance, $method)) {
return $parameters;
}
return $this->resolveMethodDependencies(
$parameters, new ReflectionMethod($instance, $method)
);
}

其中

if (! method_exists($instance, $method)) {
return $parameters;
}

为什么没报错,是考虑到 controller 也是可以用 __call 魔术方法来绑定路由的。

resolveMethodDependencies 的逻辑就是将传入的反射对象的入参解析完成

/**
* Resolve the given method's type-hinted dependencies.
*
* @param array $parameters
* @param \ReflectionFunctionAbstract $reflector
* @return array
*/
public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
$instanceCount = 0;
$values = array_values($parameters);
foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency(
$parameter, $parameters
);
if (! is_null($instance)) {
$instanceCount++;
$this->spliceIntoParameters($parameters, $key, $instance);
} elseif (! isset($values[$key - $instanceCount]) &&
$parameter->isDefaultValueAvailable()) {
$this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
}
}
return $parameters;
}

容器挂靠

紧接前文代码

/**
* Attempt to transform the given parameter into a class instance.
*
* @param \ReflectionParameter $parameter
* @param array $parameters
* @return mixed
*/
protected function transformDependency(ReflectionParameter $parameter, $parameters)
{
$class = $parameter->getClass();
// If the parameter has a type-hinted class, we will check to see if it is already in
// the list of parameters. If it is we will just skip it as it is probably a model
// binding and we do not want to mess with those; otherwise, we resolve it here.
if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
return $parameter->isDefaultValueAvailable()
? $parameter->getDefaultValue()
: $this->container->make($class->name);
}
}

[划重点,前方高能]
如果此参数为一个类,且不是路由中出现的参数,返回 默认值(如有),否则从容器创建

路由的执行

dispatch 方法的下半段

if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));

如果控制器定义了 callAction,执行。
接着直接调用 controller 的方法。

08. SessionServiceProvider 详解

前言还没想好。

服务提供者部分

SessionServiceProvider 没有 boot 方法,其 register 方法为

public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton(StartSession::class);
}

作用

下面,我们以 SESSION_DRIVER=file 继续分析
SESSION_DRIVERfile 时,会调用到 createFileDriver

protected function createFileDriver()
{
return $this->createNativeDriver();
}

createFileDriver 调用了 createNativeDriver
protected function createNativeDriver()
{
$lifetime = $this->app['config']['session.lifetime'];
return $this->buildSession(new FileSessionHandler(
$this->app['files'], $this->app['config']['session.files'], $lifetime
));
}

这里 $this->app['config']['session.files'] 得到的是

'files' => storage_path('framework/sessions'),

然后通过此处实例化 Illuminate\Session\FileSessionHandler

return $this->buildSession(new FileSessionHandler(

然后调用 buildSession 获得 \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);
}

session 功能 / 操作部分

读取 session

当触发 Session::start 时,

public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}

调用的 loadSession

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

调用的 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 [];
}

这里的 read 方法,就是 FileSessionHandler::read

public function read($sessionId)
{
if ($this->files->isFile($path = $this->path.'/'.$sessionId)) {
if ($this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
return $this->files->sharedGet($path);
}
}
return '';
}

执行到这里时, session 中的数据就被存放到了 Illuminate\Session\Store::$attributes 了。

而执行 Session::get 时,

public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}

正好就能从 Store::$attributes 中取出 session 的数据了。

写入 session

当我们使用 Illuminate\Support\Facades\Session 时,操作是调用到的 session,也就是 Illuminate\Session\SessionManager。但是在 Illuminate\Support\Manager__call 魔术方法,实现了将 session.driver 载入到 session 中被执行的能力。

public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

有兴趣的同学请阅读 15. Laravel 神奇的 Manager 类

假如我们执行 Session::put('key', 'value')
会穿透到

public function put($key, $value = null)
{
if (! is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
Arr::set($this->attributes, $arrayKey, $arrayValue);
}
}

当我们手动 Session::save 时,或者程序终止运行触发 register_shutdown_function 时,会触发

public function save()
{
$this->ageFlashData();
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}

也就是 FileSessionHandler::write

public function write($sessionId, $data)
{
$this->files->put($this->path.'/'.$sessionId, $data, true);
return true;
}

至此,写入 session 的流程走完。

05. Pipeline 和 Middleware

Laravel 的管道机制,是其中间件 (middleware) 得以被序列执行的基础

管道机制

在开始之前我们先帖一段管理的应用代码,位于 02. HTTP Kernel Handle解析

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());

语义化得出的分析为
携带 send 请求对象,经过中间件的处理,然后进入路由。

代码解析

  1. send 携带方法

    /**
    * Set the object being sent through the pipeline.
    *
    * @param mixed $passable
    * @return $this
    */
    public function send($passable)
    {
    $this->passable = $passable;
    return $this;
    }

    好像没啥出奇的,就是传入并储存一个对象,返回 $this

  2. through 经过方法

    /**
    * Set the array of pipes.
    *
    * @param array|mixed $pipes
    * @return $this
    */
    public function through($pipes)
    {
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();
    return $this;
    }

    好像也没啥关键的实现,传入“被管道”的集合,返回 $this

  3. then 异步方法

    /**
    * Run the pipeline with a final destination callback.
    *
    * @param \Closure $destination
    * @return mixed
    */
    public function then(Closure $destination)
    {
    $pipeline = array_reduce(
    array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );
    return $pipeline($this->passable);
    }

之前的都只是做登记一下就GG的作用,这个 then 才是根本大法,
使用 array_reduce 处理得到 $pipeline。这个 array_reduce 是何方神圣呢?我们查文档得到:
Iteratively reduce the array to a single value using a callback function(中文意思就是 使用回调函数,迭代地将数组减少为单个值)。

但在走到 array_reduce 之前,调用了 carry 方法 (在 5.3 及以前版本,carry 方法其实为 getSlice 方法,而过程几乎一样 来源),其内部逻辑为:

/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};
};
}

Laravel 的这个 carry 方法返回的是一个闭包,执行返回的闭包所返回的又是一个闭包,只不过前一个返回是 array_reduce 批量执行时拿到的,后面是处理原始对象(请求)时,处理中间件时候拿到的。

  • 闭包(前一个)的第一个参数 $stack 为 null 或者闭包对象,第二个参数 $pipe 为从1开始的调用顺序。
  • 闭包(前一个)的返回值,为管道中被执行的逻辑,也就是 middleware 的 handle。

具体 handle 代码我们举一个例子:

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Illuminate\Session\TokenMismatchException
*/
public function handle($request, Closure $next)
{
if (
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return tap($next($request), function ($response) use ($request) {
if ($this->addHttpCookie) {
$this->addCookieToResponse($request, $response);
}
});
}
throw new TokenMismatchException;
}

其中的 $next($request) 表示执行下一个中间件, $next 指的就是下面这个闭包

return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
list($name, $parameters) = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->container->make(Request::class))
: $response;
};

关于中间件执行的流程,是不是有眉目了?
image

等上面执行完成,Laravel 就要执行这个闭包了

/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}

也就是执行路由的 dispatch 逻辑(包含被调用的逻辑一共 82 行代码)。

/**
* Dispatch the request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
/**
* Dispatch the request to a route and return the response.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
/**
* Find the route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
/**
* Return the response for the given route.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\Route $route
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$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()
);
});
}

你可能会因为这里又有中间件而诧异

return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});

其实这里的中间件和前面的不一样,前面的是在kernel 中注册的中间件。这里的中间件是在路由绑定中注册的中间件。是两部分。

于是乎,Laravel 鬼使神差的实现了中间件的管道执行机制。

接下来,请查看 09. 容器的依赖注入机制 了解 $route->run() 背后的故事。

03. ServiceProvider Register 解析

ServiceProvider Register 解析

前面 《02. Kernel Handle解析》 结尾,我们留下了 注册服务提供者启动服务提供者 是两个比较重要的步骤的悬念,接下来两篇文章,我们来分别解析这两大流程。

代码

<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
class RegisterProviders
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$app->registerConfiguredProviders();
}
}

解析

整个 RegisterProviders 都是粮衣,其没有任何功能功能代码,而是调用到了 Illuminate\Foundation\ ApplicationregisterConfiguredProviders

注意运行时实际并没有调用 Illuminate\Contracts\Foundation\Application 的 registerConfiguredProviders,而是调用了其具体实现类 Illuminate\Foundation\ Application 的 registerConfiguredProviders。不要被上面代码中的注解表象所迷惑

/**
* Register all of the configured providers.
*
* @return void
*/
public function registerConfiguredProviders()
{
$providers = Collection::make($this->config['app.providers'])
->partition(function ($provider) {
return Str::startsWith($provider, 'Illuminate\\');
});
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
->load($providers->collapse()->toArray());
}

上面第 540-543 行是将 config/app.php 中配置的 providers

laravel/config/app.php

Lines 122 to 163 in d081c91

'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],

读取出来,判断是否为 Illuminate\ 开头,归为两组:

Illuminate\Support\Collection {#2825
     all: [
       Illuminate\Support\Collection {#2822
         all: [
            "Illuminate\Auth\AuthServiceProvider",
            "Illuminate\Broadcasting\BroadcastServiceProvider",
            "Illuminate\Bus\BusServiceProvider",
            "Illuminate\Cache\CacheServiceProvider",
            "Illuminate\Foundation\Providers\ConsoleSupportServiceProvider",
            "Illuminate\Cookie\CookieServiceProvider",
            "Illuminate\Database\DatabaseServiceProvider",
            "Illuminate\Encryption\EncryptionServiceProvider",
            "Illuminate\Filesystem\FilesystemServiceProvider",
            "Illuminate\Foundation\Providers\FoundationServiceProvider",
            "Illuminate\Hashing\HashServiceProvider",
            "Illuminate\Mail\MailServiceProvider",
            "Illuminate\Notifications\NotificationServiceProvider",
            "Illuminate\Pagination\PaginationServiceProvider",
            "Illuminate\Pipeline\PipelineServiceProvider",
            "Illuminate\Queue\QueueServiceProvider",
            "Illuminate\Redis\RedisServiceProvider",
            "Illuminate\Auth\Passwords\PasswordResetServiceProvider",
            "Illuminate\Session\SessionServiceProvider",
            "Illuminate\Translation\TranslationServiceProvider",
            "Illuminate\Validation\ValidationServiceProvider",
            "Illuminate\View\ViewServiceProvider",
         ],
       },
       Illuminate\Support\Collection {#2823
         all: [
            "22" => "App\Providers\AppServiceProvider",
            "22" => "App\Providers\AuthServiceProvider",
            "22" => "App\Providers\EventServiceProvider",
            "22" => "App\Providers\RouteServiceProvider",
         ],
       },
     ],
   }

接着 $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]) 这一行会得到如下结果

Illuminate\Support\Collection {#33
  #items: array:3 [
    0 => Illuminate\Support\Collection {#21
      #items: array:22 [
        0 => "Illuminate\Auth\AuthServiceProvider"
        1 => "Illuminate\Broadcasting\BroadcastServiceProvider"
        2 => "Illuminate\Bus\BusServiceProvider"
        3 => "Illuminate\Cache\CacheServiceProvider"
        4 => "Illuminate\Foundation\Providers\ConsoleSupportServiceProvider"
        5 => "Illuminate\Cookie\CookieServiceProvider"
        6 => "Illuminate\Database\DatabaseServiceProvider"
        7 => "Illuminate\Encryption\EncryptionServiceProvider"
        8 => "Illuminate\Filesystem\FilesystemServiceProvider"
        9 => "Illuminate\Foundation\Providers\FoundationServiceProvider"
        10 => "Illuminate\Hashing\HashServiceProvider"
        11 => "Illuminate\Mail\MailServiceProvider"
        12 => "Illuminate\Notifications\NotificationServiceProvider"
        13 => "Illuminate\Pagination\PaginationServiceProvider"
        14 => "Illuminate\Pipeline\PipelineServiceProvider"
        15 => "Illuminate\Queue\QueueServiceProvider"
        16 => "Illuminate\Redis\RedisServiceProvider"
        17 => "Illuminate\Auth\Passwords\PasswordResetServiceProvider"
        18 => "Illuminate\Session\SessionServiceProvider"
        19 => "Illuminate\Translation\TranslationServiceProvider"
        20 => "Illuminate\Validation\ValidationServiceProvider"
        21 => "Illuminate\View\ViewServiceProvider"
      ]
    }
    1 => array:5 [
      0 => "BeyondCode\DumpServer\DumpServerServiceProvider"
      1 => "Fideloper\Proxy\TrustedProxyServiceProvider"
      2 => "Laravel\Tinker\TinkerServiceProvider"
      3 => "Carbon\Laravel\ServiceProvider"
      4 => "NunoMaduro\Collision\Adapters\Laravel\CollisionServiceProvider"
    ]
    2 => Illuminate\Support\Collection {#31
      #items: array:4 [
        22 => "App\Providers\AppServiceProvider"
        23 => "App\Providers\AuthServiceProvider"
        24 => "App\Providers\EventServiceProvider"
        25 => "App\Providers\RouteServiceProvider"
      ]
    }
  ]
}

最后一句代码

(new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) 
                     ->load($providers->collapse()->toArray()); 

第一步是将加载缓存好的 bootstrap/cache/services.php

/**
* Get the path to the cached services.php file.
*
* @return string
*/
public function getCachedServicesPath()
{
return $this->bootstrapPath().'/cache/services.php';
}

其实就是执行 ProviderRepository::load() $providers-> collapse() 得到的数组 (这个数组是前面 splice 拆分后再并入的)。

/**
* Register the application service providers.
*
* @param array $providers
* @return void
*/
public function load(array $providers)
{
$manifest = $this->loadManifest();
// First we will load the service manifest, which contains information on all
// service providers registered with the application and which services it
// provides. This is used to know which services are "deferred" loaders.
if ($this->shouldRecompile($manifest, $providers)) {
$manifest = $this->compileManifest($providers);
}
// Next, we will register events to load the providers for each of the events
// that it has requested. This allows the service provider to defer itself
// while still getting automatically loaded when a certain event occurs.
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}
// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
$this->app->register($provider);
}
$this->app->addDeferredServices($manifest['deferred']);
}

这句 loadManifest 即为加载前面传入的 bootstrap/cache/services.php

/**
* Load the service provider manifest JSON file.
*
* @return array|null
*/
public function loadManifest()
{
// The service manifest is a file containing a JSON representation of every
// service provided by the application and whether its provider is using
// deferred loading or should be eagerly loaded on each request to us.
if ($this->files->exists($this->manifestPath)) {
$manifest = $this->files->getRequire($this->manifestPath);
if ($manifest) {
return array_merge(['when' => []], $manifest);
}
}
}

第60行调用到的的 shouldRecompile 逻辑为判断是否 provider 不符合,代码

/**
* Determine if the manifest should be compiled.
*
* @param array $manifest
* @param array $providers
* @return bool
*/
public function shouldRecompile($manifest, $providers)
{
return is_null($manifest) || $manifest['providers'] != $providers;
}

如果需要重新编译此服务提供者缓存的 bootstrap/cache/services.php ,把数据 $manifest 提取到 bootstrap/cache/services.php ,具体过程为:

/**
* Compile the application service manifest file.
*
* @param array $providers
* @return array
*/
protected function compileManifest($providers)
{
// The service manifest should contain a list of all of the providers for
// the application so we can compare it on each request to the service
// and determine if the manifest should be recompiled or is current.
$manifest = $this->freshManifest($providers);
foreach ($providers as $provider) {
$instance = $this->createProvider($provider);
// When recompiling the service manifest, we will spin through each of the
// providers and check if it's a deferred provider or not. If so we'll
// add it's provided services to the manifest and note the provider.
if ($instance->isDeferred()) {
foreach ($instance->provides() as $service) {
$manifest['deferred'][$service] = $provider;
}
$manifest['when'][$provider] = $instance->when();
}
// If the service providers are not deferred, we will simply add it to an
// array of eagerly loaded providers that will get registered on every
// request to this application instead of "lazy" loading every time.
else {
$manifest['eager'][] = $provider;
}
}
return $this->writeManifest($manifest);
}

/**
* Write the service manifest file to disk.
*
* @param array $manifest
* @return array
*
* @throws \Exception
*/
public function writeManifest($manifest)
{
if (! is_writable(dirname($this->manifestPath))) {
throw new Exception('The bootstrap/cache directory must be present and writable.');
}
$this->files->put(
$this->manifestPath, '<?php return '.var_export($manifest, true).';'
);
return array_merge(['when' => []], $manifest);
}

到这时,ProviderRepository::load() 的服务提供者缓存的判断和执行流程就结束了。我们回到 ProviderRepository::load() 的后面流程,接下来就是

// Next, we will register events to load the providers for each of the events
// that it has requested. This allows the service provider to defer itself
// while still getting automatically loaded when a certain event occurs.
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}

没错,触发 registerLoadEvents 方法了。感谢 laravel 代码做到了足够语义化,我们猜到了这里就是触发服务提供者注册事件的。具体流程为:

/**
* Register the load events for the given provider.
*
* @param string $provider
* @param array $events
* @return void
*/
protected function registerLoadEvents($provider, array $events)
{
if (count($events) < 1) {
return;
}
$this->app->make('events')->listen($events, function () use ($provider) {
$this->app->register($provider);
});
}

  • 若需关注更多关于 Laravel 事件触发机制,请关注 [TODO]

再继续后面,就是 ProviderRepository::load() 触发容器的 register 的流程了

// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
$this->app->register($provider);
}

Illuminate\Foundation\Container\Appplication 容器的 register 方法代码为

/**
* Register a service provider with the application.
*
* @param \Illuminate\Support\ServiceProvider|string $provider
* @param bool $force
* @return \Illuminate\Support\ServiceProvider
*/
public function register($provider, $force = false)
{
if (($registered = $this->getProvider($provider)) && ! $force) {
return $registered;
}
// If the given "provider" is a string, we will resolve it, passing in the
// application instance automatically for the developer. This is simply
// a more convenient way of specifying your service provider classes.
if (is_string($provider)) {
$provider = $this->resolveProvider($provider);
}
if (method_exists($provider, 'register')) {
$provider->register();
}
// If there are bindings / singletons set as properties on the provider we
// will spin through them and register them with the application, which
// serves as a convenience layer while registering a lot of bindings.
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$this->singleton($key, $value);
}
}
$this->markAsRegistered($provider);
// If the application has already booted, we will call this boot method on
// the provider class so it has an opportunity to do its boot logic and
// will be ready for any usage by this developer's application logic.
if ($this->booted) {
$this->bootProvider($provider);
}
return $provider;
}

步骤

  1. 先尝试取 getProvider 这个服务,如果能取到证明注册过了,除非要求强硬方式 $force 就不再注册
  2. 解析出 $provier 为具体的 ServiceProvider 对象
  3. 判断 $provider 有无 register 方法,有则运行
  4. 判断 $provider有无定义 $bindings 属性,若有,则依次 $key$value 为参,进入 Illuminate\Foundation\Container\Appplication::bind($key, $value) 执行
  5. 判断 $provider 有无定义 $singletons 属性,若有,则依次 $key$value 为参,进入 Illuminate\Foundation\Container\Appplication::singleton($key, $value) 执行
  6. 标记这个服务提供者已被注册过了 Illuminate\Foundation\Container\Appplication::$loadedProviders[ServiceProvider::class] = true
  7. 如果服务提供者已经被启动过了 ($booted == true), 调用 bootProvider($provider)

15. Laravel 神奇的 Manager 类

Laravel 有不少门面类,如 SessionView 初一看令人诈舌,这些类的功能类不只一个,那么 Laravel 是怎么让这些奇葩的门面通往不止一个后门的?

类声明

namespace Illuminate\Support;
use Closure;
use InvalidArgumentException;
abstract class Manager
{

这是一个抽象类。

属性

/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
/**
* The registered custom driver creators.
*
* @var array
*/
protected $customCreators = [];
/**
* The array of created "drivers".
*
* @var array
*/
protected $drivers = [];

$app 是容器,这个 $customCreators 是什么鬼没看懂我们可以不管它。后面这个 $drivers,是一个数组,放的什么东西呢?

driver 在英语中是驱动器的意思。

算了。。。再这么讲下去太拖了。我们拉到最后

魔术函数

public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}

我们看到,当调用到当前实现类不存在方法时会调用 $this->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 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];
}

然后 getDefaultDriver

abstract public function getDefaultDriver();

这是抽象函数,我们到具体的例子中看吧。比如 session

public function getDefaultDriver()
{
return $this->app['config']['session.driver'];
}


所以,这些继承于 Illuminate\Support\Manager 的门后类,实现了向 driver 的穿透。

总结: Manager first, driver end. 经理先行,司机垫后

14. Response 对象和 Kernel::terminate

在前面 01. HTTP 入口解析 的第 54-56 行代码分析时

laravel/public/index.php

Lines 54 to 56 in 7028b17

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

我们只分析了 $request 的产生,却没有分析 $response
这节,我们就来研究下 Laravel 的 Response 对象: Illuminate\Http\Response

Illuminate\Http\Response 初识

不难看出,这句代码拿到的一定是个对象。为什么?
因为在下文,就调用了他的 ->send() 方法。

$response->send();

那么 Illuminate\Http\Response 对象是从哪里来的呢?

在从 入口Kernel::handle()管道 的过程尾声,我们拿到了这句逻辑

return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});

return $this->prepareResponse(
$request, $route->run()
);

这个 prepareResponse 是干嘛的呢?

public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}

调用了

public static function toResponse($request, $response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse($request);
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
$response = new JsonResponse($response, 201);
} elseif (! $response instanceof SymfonyResponse &&
($response instanceof Arrayable ||
$response instanceof Jsonable ||
$response instanceof ArrayObject ||
$response instanceof JsonSerializable ||
is_array($response))) {
$response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
}

过程为:

  • 判断是否为 PsrResponseInterface 对象;
  • 则判断为模型,且刚创建,将响应头 statusCoe 强制覆盖成 HTTP 201 created
  • 否则判断返回的对象是否具有序列号特征,如果是就重置成 JsonResponse
  • 否则的话覆盖成 Response

于是乎,我们才可以在 controller 中大胆的直接返回字符串而转换成 Response 对象。因为在这里他帮我们转了。

经过上面这一波加工后,在 public/index.php 里面才敢执行 $response->send()

Illuminate\Http\Response::send() 的逻辑

public function send()
{
$this->sendHeaders();
$this->sendContent();
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}

调用的 sendContent 逻辑为

public function sendContent()
{
echo $this->content;
return $this;
}
其中的 echo 代码清晰可见,非常简单。

先于 sendContentsendHeaders 的代码要稍微复杂一点

public function sendHeaders()
{
// headers have already been sent by the developer
if (headers_sent()) {
return $this;
}
// headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
}
// cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode);
}
// status
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
return $this;
}

先是将 http 响应头依次输出,然后将 Cookie 处理后输出。

send 方法的后面,还有几句

if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
作用是判断是否是 fastcgi 或者 cli 模式调用,如果是就关闭连接、刷新缓冲区。

至此,send 逻辑就讲完了。

Kernel::terminate() 的逻辑

App\Http\Kernel 并没有 terminate 方法,一路穿透到了 Illuminate\Foundation\Http\Kernel 里面

public function terminate($request, $response)
{
$this->terminateMiddleware($request, $response);
$this->app->terminate();
}

Illuminate\Foundation\Http\Kernel::terminateMiddleware() 逻辑

protected function terminateMiddleware($request, $response)
{
$middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
$this->gatherRouteMiddleware($request),
$this->middleware
);
foreach ($middlewares as $middleware) {
if (! is_string($middleware)) {
continue;
}
list($name) = $this->parseMiddleware($middleware);
$instance = $this->app->make($name);
if (method_exists($instance, 'terminate')) {
$instance->terminate($request, $response);
}
}
}

将容器中 bind 过的中间件 terminate() 掉。

而这句就比较好理解了,直接调用了容器的 terminate()

容器的 terminate() 方法也比较简单,触发 $terminatingCallbacks 注册的回调。

public function terminate()
{
foreach ($this->terminatingCallbacks as $terminating) {
$this->call($terminating);
}
}

PHP的不常驻特性,一个请求完了就自动回收资源,那么除了需要触发 $terminatingCallbacks 回调外,为什么还要实现 terminate() 呢?

Laravel 为我们提供了一个 php artisan serve 的便捷服务器,启动后 Application 对象是常驻的。所以需要单独回收需要回收的资源。

php artisan serve 的本质也是 php -S,详细资料 PHP 的命令行模式>内置Web Server

扩展阅读:在 Laravel 5.3 以前,Kernel::terminate() 还调用了 fastcgi_finish_request()。后来移除了,主要原因是因为这个函数会阻断 http 响应的头和 body,造成部分特殊情况的 session 问题,不过也可能还有其他原因。

10. 容器的 singleton 和 bind 的实现

容器的 singletonbind 方法在整个 Laravel 框架或扩展中是调用的比较频繁的底层方法,掌握其原理能帮助我们加深 laravel 容器的理解。

初识

singleton() 代码

单身狗模式

/**
* Register a shared binding in the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}

bind() 代码

分裂模式

/**
* Register a binding with the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}

初略看二者差异可能看不大出来。
我们从他们暴露的接口入手,先用,再啪(扒)源码。

调试

祭出大杀器:php artisan tinker

bind

>>> app()->bind('test', function(){dump(1); return new \StdClass();});
1
=> null
>>>
>>> app()->make('test')
1
=> {#2895}
>>>
>>> app()->make('test')
1
=> {#2896}

可以看出,调用 bind 时,每次取出被绑定的对象时,都会重新去构建一次。

如果用singleton 呢。

singleton

>>> app()->singleton('test', function(){dump(1); return new \StdClass();});
1
=> null
>>> app()->make('test')
=> {#2915}
>>> app()->make('test')
=> {#2915}
>>>

分析

bind 的实质,容器中捆绑

我们回头再看下 bind() 方法

/**
* Register a binding with the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}

逻辑步骤为:

  • 参数依次为 $abstract$concrete
  • dropStaleInstances 移除之前 bind 过的 $abstract 的数据(旧的 $concret
  • 如果没有传 $concrete,则将 $concrete 设置成 $abstract
  • 如果为闭包 如果不为闭包(感谢 @HubQin 的指正),则通过 Container::getClosure() 方法将闭包改成一个接受 $container$parameters 的包裹后的闭包。此闭包实质就是在需要运行的时候,运行闭包。
  • Container::$bindings 数组 $abstract 为 key 的值设置为 compact('concrete', 'shared'),即 ["concrete" => $concrete, "shared" => $shared]
  • 如果 bind 的对象已经 resolved 过了,通过 Container::rebound() 触发 reboundCallbacks 中的回调。

到目前为止,暂时还没看出 $sharesingletonbind 的影响。只知道 bind 在执行过程中,将 $shared$concrete 一起存到了 Container::$bindings 中。

用句四川话,莫慌,我们马上就要揭开关键的面纱了。。。

揭开 Container::make() 神秘的面纱

/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}

其实 make 是直接透传给了 resolve

/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}

第一步骤的

$abstract = $this->getAlias($abstract);

是做了森么呢?
还原用 Container::alias() 方法设置过别名的原始 $abstract

下一步,

$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);

Container::getContextualConcrete() 其实是取这里设置进取的 上下文绑定 的数据

/**
* Define the implementation for the contextual binding.
*
* @param \Closure|string $implementation
* @return void
*/
public function give($implementation)
{
$this->container->addContextualBinding(
$this->concrete, $this->needs, $implementation
);
}

ContextualBindingBuilder::give() 暂时就不在这里暂开研究了。咱们继续往下看

$needsContextualBuild 值为 make() 方法是否传了第二个参数,也就是 $parametersneeds contextual build 意思是

构建时,是否具有上下文关联性

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}

判断 Container::$instances 数组中是否能通过 $abstract 找到对象。如果找得到且不具有上下文关联性,就用之前创建好的对象。

$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);

getConcrete() 代码为

* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}

相信你能看出来 return $this->bindings[$abstract]['concrete'] ,就是根据 $abstract 把前面 bindsingleton 的数据返回。

这里的 isBuildable 是判断 $abstract 是否与 $concrete 一致或 $concrete 为闭包

如果不是,那么可能是还有递归调用,需要再走一次 make

if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}

645 行调用的 isBuildable()

protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}

接着调用了 Container::build() 方法

public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}

其中的

$reflector = new ReflectionClass($concrete);
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
是通过反射解析待构建类所需参数中其他对象参数,从容器中取出填入。

再往下面就是执行 Container::extend 设置的扩充流程了。

foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}

再后面,才是对我们 singleton()bind() 产生深远影响的代码:

if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}

我们在前面知道,如果为 singleton() 执行到 resolve() 的第二个参数 $sharedtrue, bindfalse
前面 $needsContextualBuild 判断的位置,判断了 Container::$instance 中是否有 $abstract, 所以在这里如果 resolve 没有将 $object 绑入 Container::$instance,那么下次 Container::make() 依旧会在去 build() 一次,这就产生了 singleton()bind()的特性。

再接着,就是触发事件了。

$this->fireResolvingCallbacks($abstract, $object);

最后是一些收尾工作

$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;

此章节为什么叫做单身狗呢?因为 Container::make() 严格遵守 $shared,如果声明了 true,他保证不搞一个分身(所以是单身)给你。

04. ServiceProvider Boot 解析

随着前面 《02. Kernel Handle解析》 和 《03. ServiceProvider Register 解析》 的结束,我们接下来要分析的便是 启动服务提供者 这个步骤。

代码

<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
class BootProviders
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
$app->boot();
}
}

ServiceProvider Register 一样的,在 BootProviders 中也是调用了 Illuminate\Foundation\Application::boot()

/**
* Boot the application's service providers.
*
* @return void
*/
public function boot()
{
if ($this->booted) {
return;
}
// Once the application has booted we will also fire some "booted" callbacks
// for any listeners that need to do work after this initial booting gets
// finished. This is useful when ordering the boot-up processes we run.
$this->fireAppCallbacks($this->bootingCallbacks);
array_walk($this->serviceProviders, function ($p) {
$this->bootProvider($p);
});
$this->booted = true;
$this->fireAppCallbacks($this->bootedCallbacks);
}

首先判断是否 boot 过。

然后通过触发 $bootingCallbacks 钩子

/**
* Call the booting callbacks for the application.
*
* @param array $callbacks
* @return void
*/
protected function fireAppCallbacks(array $callbacks)
{
foreach ($callbacks as $callback) {
call_user_func($callback, $this);
}
}

$bootingCallbacks 是来自 02. HTTP Kernel Handle解析 登记的闭包,主要是服务于声明了 $defer = true 的服务提供者。

然后依次遍历 $this->serviceProviders 执行 Illuminate\Foundation\Application::bootProvider()

/**
* Boot the given service provider.
*
* @param \Illuminate\Support\ServiceProvider $provider
* @return mixed
*/
protected function bootProvider(ServiceProvider $provider)
{
if (method_exists($provider, 'boot')) {
return $this->call([$provider, 'boot']);
}
}

其实就是运行一遍 ServiceProvider 的 boot 方法

接着将 Illuminate\Foundation\Application::$boot 设置为 true

最后触发 bootedCallbacks 钩子

17. 通知/广播 (Broadcast) 机制

鉴权

config/app.php 中我们能看到 Illuminate\Broadcasting\BroadcastServiceProvider 这个服务提供者被注册:

Illuminate\Broadcasting\BroadcastServiceProvider::class,

在前面对服务提供者的讲解中,我们得出了一个服务提供者肯定会包含 bootregister 方法之一,这 BroadcastServiceProvider 就是一个包含而且只包含了 boot 方法的服务提供者:
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes();
require base_path('routes/channels.php');
}

第17行调用到了 Illuminate\Support\Facades\Broadcast::routes() 假面方法。

顾名思义,就是注册路由的。而这个方法,最终是穿透到了 Illuminate\Broadcasting\BroadcastManager::routes()

/**
* Register the routes for handling broadcast authentication and sockets.
*
* @param array|null $attributes
* @return void
*/
public function routes(array $attributes = null)
{
if ($this->app->routesAreCached()) {
return;
}
$attributes = $attributes ?: ['middleware' => ['web']];
$this->app['router']->group($attributes, function ($router) {
$router->match(
['get', 'post'], '/broadcasting/auth',
'\\'.BroadcastController::class.'@authenticate'
);
});
}

第19行是为了在 config/app.php 卸载 BroadcastServiceProvider 时,直观的让 routes/channels.php 失效的做法,所以就不放在别处

require base_path('routes/channels.php');

第61~63行是如果缓存过路由,则跳出。

第62~第72行相当于与执行了

Route::group(['middleware' => ['web']], function () {
    Route::match(['get', 'post'], '/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate');
});

方法调用到的 controller action 其实是

/**
* Authenticate the request for channel access.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function authenticate(Request $request)
{
return Broadcast::auth($request);
}

这个假面方法 Broadcast::auth() 比较特殊,是根据 config('broadcasting.default') 分发到 Illuminate/Broadcasting/Broadcasters 具体的类中:

  • LogBroadcaster.php
  • NullBroadcaster.php
  • PusherBroadcaster.php
  • RedisBroadcaster.php

我们以 redis 的配置为例 (laravel-echo-server) 。

/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
throw new AccessDeniedHttpException;
}
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
}

第一步,如果 $request->channel_name 不以 private- 或者 presence- 开头,或者没有提供用户授权导致请求取不到用户,抛出 Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException 异常

第二步,将 $request->channel_name 中的 private- 或者 presence- 还原移除。

第三步,调用 Broadcaster:: verifyUserCanAccessChannel()

/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @param string $channel
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function verifyUserCanAccessChannel($request, $channel)
{
foreach ($this->channels as $pattern => $callback) {
if (! Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel)) {
continue;
}
$parameters = $this->extractAuthParameters($pattern, $channel, $callback);
$handler = $this->normalizeChannelHandlerToCallable($callback);
if ($result = $handler($request->user(), ...$parameters)) {
return $this->validAuthenticationResponse($request, $result);
}
}
throw new AccessDeniedHttpException;
}

routes/channels.php 中定义的 channel_name 匹配上的通道的鉴权回调执行,拿到返回值。如果 if ($result = $handler($request->user(), ...$parameters)) 返回成功的 RedisBroadcaster::validAuthenticationResponse(),否则抛出 Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException 异常

而成功的响应 RedisBroadcaster::validAuthenticationResponse() 逻辑为返回 user_iduser_info 给 websocket 客户端:

/**
* Return the valid authentication response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $result
* @return mixed
*/
public function validAuthenticationResponse($request, $result)
{
if (is_bool($result)) {
return json_encode($result);
}
return json_encode(['channel_data' => [
'user_id' => $request->user()->getAuthIdentifier(),
'user_info' => $result,
]]);
}

至此,鉴权部分完成。


推送

broadcast() 方法定义于

function broadcast($event = null)
{
return app(BroadcastFactory::class)->event($event);
}

此方法最终走到了

/**
* Begin broadcasting an event.
*
* @param mixed|null $event
* @return \Illuminate\Broadcasting\PendingBroadcast|void
*/
public function event($event = null)
{
return new PendingBroadcast($this->app->make('events'), $event);
}

PendingBroadcast 类比较特殊,有个 __destruct 解构方法:

/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
$this->events->dispatch($this->event);
}

走到

/**
* Fire an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
* @return array|null
*/
public function dispatch($event, $payload = [], $halt = false)
{
// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
list($event, $payload) = $this->parseEventAndPayload(
$event, $payload
);
if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}
$responses = [];
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && ! is_null($response)) {
return $response;
}
// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
break;
}
$responses[] = $response;
}
return $halt ? null : $responses;
}

调用了

/**
* Broadcast the given event class.
*
* @param \Illuminate\Contracts\Broadcasting\ShouldBroadcast $event
* @return void
*/
protected function broadcastEvent($event)
{
$this->container->make(BroadcastFactory::class)->queue($event);
}

还是回到了 BroadcastManager

/**
* Queue the given event for broadcast.
*
* @param mixed $event
* @return void
*/
public function queue($event)
{
$connection = $event instanceof ShouldBroadcastNow ? 'sync' : null;
if (is_null($connection) && isset($event->connection)) {
$connection = $event->connection;
}
$queue = null;
if (method_exists($event, 'broadcastQueue')) {
$queue = $event->broadcastQueue();
} elseif (isset($event->broadcastQueue)) {
$queue = $event->broadcastQueue;
} elseif (isset($event->queue)) {
$queue = $event->queue;
}
$this->app->make('queue')->connection($connection)->pushOn(
$queue, new BroadcastEvent(clone $event)
);
}

根据前文的 config('broadcasting.default') 配置,

第127~129行,是将广播事件 BroadcastEvent 入队。

根据我们对队列的了解,BroadcastEvent 应该有 handle() 方法

/**
* Handle the queued job.
*
* @param \Illuminate\Contracts\Broadcasting\Broadcaster $broadcaster
* @return void
*/
public function handle(Broadcaster $broadcaster)
{
$name = method_exists($this->event, 'broadcastAs')
? $this->event->broadcastAs() : get_class($this->event);
$broadcaster->broadcast(
Arr::wrap($this->event->broadcastOn()), $name,
$this->getPayloadFromEvent($this->event)
);
}

第46~49行,是执行广播,调用了 RedisBroadcaster::broadcast()

/**
* Broadcast the given event.
*
* @param array $channels
* @param string $event
* @param array $payload
* @return void
*/
public function broadcast(array $channels, $event, array $payload = [])
{
$connection = $this->redis->connection($this->connection);
$payload = json_encode([
'event' => $event,
'data' => $payload,
'socket' => Arr::pull($payload, 'socket'),
]);
foreach ($this->formatChannels($channels) as $channel) {
$connection->publish($channel, $payload);
}
}

消息进入 redis。

// TODO: laravel-echo-server 部分。

06. RouteServiceProvider 详解

RouteServiceProvider 详解

RouteServiceProvider::boot 阶段,所有路由都只是执行登记,匹配的逻辑是在 02. HTTP Kernel Handle解析 的 dispatchToRouter 阶段。

我们先找到 RouteServiceProvider.php

config/app.phpproviders 中定义的 RouteServiceProvider 其实是:

App\Providers\RouteServiceProvider::class,

而这个类其实继承自 Illuminate\Foundation\Support\Providers\RouteServiceProvider
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{

我们分析 Illuminate\Foundation\Support\Providers\RouteServiceProvider 的代码

register() 阶段

register 方法是空的

/**
* Register the service provider.
*
* @return void
*/
public function register()
{
//
}

boot() 阶段

而在 boot 阶段,几乎运行了 RouteServiceProvider 中的所有方法

/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$this->setRootControllerNamespace();
if ($this->app->routesAreCached()) {
$this->loadCachedRoutes();
} else {
$this->loadRoutes();
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
}

一. setRootControllerNamespace()

/**
* Set the root controller namespace for the application.
*
* @return void
*/
protected function setRootControllerNamespace()
{
if (! is_null($this->namespace)) {
$this->app[UrlGenerator::class]->setRootControllerNamespace($this->namespace);
}
}

二. 判断路由是否缓存过

Illuminate\Foundation\Application::routesAreCached
其实就是判断文件 bootstrap/cache/routes.php 是否存在

/**
* Determine if the application routes are cached.
*
* @return bool
*/
public function routesAreCached()
{
return $this['files']->exists($this->getCachedRoutesPath());
}

/**
* Get the path to the routes cache file.
*
* @return string
*/
public function getCachedRoutesPath()
{
return $this->bootstrapPath().'/cache/routes.php';
}

存在则直接加载
/**
* Load the cached routes for the application.
*
* @return void
*/
protected function loadCachedRoutes()
{
$this->app->booted(function () {
require $this->app->getCachedRoutesPath();
});
}

三. 如果没有缓存,则遍历路由

/**
* Load the application routes.
*
* @return void
*/
protected function loadRoutes()
{
if (method_exists($this, 'map')) {
$this->app->call([$this, 'map']);
}
}

map 方法在这里
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}

/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}

这里的 mapWebRoutesmapApiRoutes 是分别将 routes/web.php 和 routes/api.php 用 Route 门面类的 group 加载了一遍。 Route::group 实质运行到的是 Illuminate\Routing\Router::group (关于门面类的文章请见 [TODO]),代码为
/**
* Create a route group with shared attributes.
*
* @param array $attributes
* @param \Closure|string $routes
* @return void
*/
public function group(array $attributes, $routes)
{
$this->updateGroupStack($attributes);
// Once we have updated the group stack, we'll load the provided routes and
// merge in the group's attributes when the routes are created. After we
// have created the routes, we will pop the attributes off the stack.
$this->loadRoutes($routes);
array_pop($this->groupStack);
}
/**
* Update the group stack with the given attributes.
*
* @param array $attributes
* @return void
*/
protected function updateGroupStack(array $attributes)
{
if (! empty($this->groupStack)) {
$attributes = RouteGroup::merge($attributes, end($this->groupStack));
}
$this->groupStack[] = $attributes;
}
/**
* Merge the given array with the last group stack.
*
* @param array $new
* @return array
*/
public function mergeWithLastGroup($new)
{
return RouteGroup::merge($new, end($this->groupStack));
}
/**
* Load the provided routes.
*
* @param \Closure|string $routes
* @return void
*/
protected function loadRoutes($routes)
{
if ($routes instanceof Closure) {
$routes($this);
} else {
$router = $this;
require $routes;
}
}

上面的逻辑是一层层剥开 Route::group 去执行里面的 Route::get / Route::post ...
* Register a new GET route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
/**
* Register a new POST route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function post($uri, $action = null)
{
return $this->addRoute('POST', $uri, $action);
}
/**
* Register a new PUT route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function put($uri, $action = null)
{
return $this->addRoute('PUT', $uri, $action);
}
/**
* Register a new PATCH route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function patch($uri, $action = null)
{
return $this->addRoute('PATCH', $uri, $action);
}
/**
* Register a new DELETE route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function delete($uri, $action = null)
{
return $this->addRoute('DELETE', $uri, $action);
}
/**
* Register a new OPTIONS route with the router.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function options($uri, $action = null)
{
return $this->addRoute('OPTIONS', $uri, $action);
}
/**
* Register a new route responding to all verbs.
*
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function any($uri, $action = null)
{
return $this->addRoute(self::$verbs, $uri, $action);
}

不难发现,不管是 GET/POST... 还是 any,都只是调用了 addRoute 将这个路由的属性登记了一下:
/**
* Add a route to the underlying route collection.
*
* @param array|string $methods
* @param string $uri
* @param \Closure|array|string|null $action
* @return \Illuminate\Routing\Route
*/
public function addRoute($methods, $uri, $action)
{
return $this->routes->add($this->createRoute($methods, $uri, $action));
}

$this->routes 是一个 \Illuminate\Routing\RouteCollection 的集合类,add 由这个方法生成的路由
/**
* Create a new route instance.
*
* @param array|string $methods
* @param string $uri
* @param mixed $action
* @return \Illuminate\Routing\Route
*/
protected function createRoute($methods, $uri, $action)
{
// If the route is routing to a controller we will parse the route action into
// an acceptable array format before registering it and creating this route
// instance itself. We need to build the Closure that will call this out.
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
// If we have groups that need to be merged, we will merge them now after this
// route has already been created and is ready to go. After we're done with
// the merge we will be ready to return the route back out to the caller.
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
$this->addWhereClausesToRoute($route);
return $route;
}

四. dispatch() -> runRoute() 阶段

路由登记完成后,就是 Kernel 触发管道层层剥洋葱调用中间件最后触发路由 dispatch (当然,这已经运行到 RouteServiceProvider 的外面了)。

剥洋葱的过程请查阅 05. Pipeline 解析

/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$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()
);
});
}

第679行的 $route->run 是至关重要的,调用到了 controller (本质其实是使用 09. 容器的依赖注入机制 将路由方法所依赖参数解析出来,并运行)

/**
* Run the route action and return the response.
*
* @return mixed
*/
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.