Laravel 源码阅读之二:Illuminate\Foundation\Application 类初始化分析

从入口文件public/index.php看,Composer自动加载之后,程序接着执行:

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

这里获得一个Illuminate\Foundation\Application类的实例$app,而Application继承自Container容器类,所以$app实例可以调用Container类的所有非私有方法,也就是说它具备了“容器”的能力。

bootstrap/app.php文件分析

.
.
.

//实例化Application类,传入参数为项目目录路径,如:/home/vagrant/code/Laravel
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

//绑定类的实现到接口,使得依赖注入是`Illuminate\Contracts\Http\Kerne`接口类
//可以得到`App\Http\Kernel::class`类,另外两个绑定同理。
$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
);
//返回实例
return $app;

Application类的初始化

new一个Application对象的时候,将触发Application类的构造函数。

构造函数分析

public function __construct($basePath = null)
    {
        if ($basePath) {
            //(A)路径绑定
            //设置basePath,即项目目录(非站点根目录),
            //其路径形如:/home/vagrant/code/Laravel
            //setBasePath方法内部还调用了bindPathsInContainer方法,其方法分析见(A)部分
            $this->setBasePath($basePath);
        }
        //(B)基础绑定
        $this->registerBaseBindings();
        //(C)基础服务提供者绑定,结果绑定在$this->serverProvider中
        $this->registerBaseServiceProviders();
        //(D)核心别名绑定,结果绑定在$this->aliases和$this->abstractAliases中
        $this->registerCoreContainerAliases();
    }

(A)bindPathsInContainer方法分析

protected function bindPathsInContainer()
{
    //path()方法用于给项目目录路径后缀'/app'+传入的$path,这里$path='',
    //最后得到项目的app路径
    $this->instance('path', $this->path());
    $this->instance('path.base', $this->basePath());
    $this->instance('path.lang', $this->langPath());
    $this->instance('path.config', $this->configPath());
    $this->instance('path.public', $this->publicPath());
    $this->instance('path.storage', $this->storagePath());
    $this->instance('path.database', $this->databasePath());
    $this->instance('path.resources', $this->resourcePath());
    $this->instance('path.bootstrap', $this->bootstrapPath());
}

理解该方法的关键是理解Container类的instance方法。instance代码如下:

public function instance($abstract, $instance)
{
    //移除类的标识的别名(如果有的话)
    $this->removeAbstractAlias($abstract);

    //bound()方法的实现只有这句:
    //return isset($this->deferredServices[$abstract]) || parent::bound($abstract);
    //判断是否有绑定
    $isBound = $this->bound($abstract);
    //去掉对应别名
    unset($this->aliases[$abstract]);

    //保存传入的值到instances变量
    $this->instances[$abstract] = $instance;

    if ($isBound) {
        $this->rebound($abstract);
    }

    return $instance;
}

instance方法的执行过程还是比较不好理解,绕来绕去的,看不明白为什么要bound、rebound、unset等,但方法最终要实现的目标比较简单,我们直接来看看bindPathsInContainer方法的执行结果:
file
就是将应用中一些基本的路径保存到instances。到此,构造函数中$this->setBasePath($basePath);的工作才算完成。

(B)registerBaseBindings方法分析

protected function registerBaseBindings()
{
    //B-1
    static::setInstance($this);
    //B-2
    $this->instance('app', $this);
    //与B-2类似
    $this->instance(Container::class, $this);
    //B-3
    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}
B-1 setInstance方法
 public static function setInstance(ContainerContract $container = null)
{
    return static::$instance = $container;
}

先来看传入的$this是否符合类型约束。static::setInstance($this);中,传入的$this$app,属于Application类的实例,而该类继承了Container类,Container类实现了ContainerContact接口,所以$this instanceof ContainerContact的值为true,即$this作为参数传入setInstance符合类型约束。
该方法将传入的$this绑定到父类Container$instance成员变量,看起来非常奇怪,结果也是挺奇怪的——得到一个可以无穷展开的instance变量:
file

B-2

这一步也是得到奇怪的结果:
file
$instances中有$app$app中又有$instances,如此无限循环嵌套。

B-3

运行结果如下:
file
这一步将一个PackageManifest实例添加到$instances中。至此,registerBaseBindings的工作完成。

(C) registerBaseServiceProviders方法分析

protected function registerBaseServiceProviders()
{
    //C-1 注册事件服务
    $this->register(new EventServiceProvider($this));
    //C-2 注册日志服务
    $this->register(new LogServiceProvider($this));
    //C-3 注册路由服务
    $this->register(new RoutingServiceProvider($this));
}
C-1

EventServiceProvider类继承自ServiceProvider,但自身没有构造函数,所以new一个对象的时候,使用ServiceProvider的构造函数,其构造函数如下:

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

该构造函数将传入的实例绑定到自身的成员变量中。C-2和C-3同理。

register方法分析
public function register($provider, $force = false)
{
    //C-4
    //假设$provider='Illuminate\Events\EventServiceProvider'类的实例
    //这里$registered=null
    if (($registered = $this->getProvider($provider)) && ! $force) {
        return $registered;
    }

    //C-5
    //$provider不是字符串,是一个类的实例
    if (is_string($provider)) {
        $provider = $this->resolveProvider($provider);
    }
    //判断该类是否存在‘register’方法,有则的调用它
    if (method_exists($provider, 'register')) {
        $provider->register();
    }

    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;
}
C-4

getProvider方法:

public function getProvider($provider)
{
    //判断是否有服务提供者,有则返回该类的标识,没有则返回null
    return array_values($this->getProviders($provider))[0] ?? null;
}

getProviders方法:

public function getProviders($provider)
{
    //$name='Illuminate\Events\EventServiceProvider'
    $name = is_string($provider) ? $provider : get_class($provider);
    //判断$this->serviceProviders的每一个元素是否是$name的实例,是则保留
    return Arr::where($this->serviceProviders, function ($value) use ($name) {
        return $value instanceof $name;
    });
}

同样的,过程很绕,封装很深,有种进得去出不来的感觉,让人喘不过气。不过,最终达到的目标也很简单:
file
将三个服务提供者保存到$serviceProviders$loadedProviders中。

(D)registerCoreContainerAliases方法分析

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],
           .
           .
           .
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }

其中的alias方法:

public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $abstract;

        $this->abstractAliases[$abstract][] = $alias;
    }

这一步比较简单,目的是给核心类的的命名空间设置别名,这样以后使用的时候就不用敲一长串的命名空间了。执行结果如下:
file
file
以上,一个是命名空间到别名的映射,一个是别名到命名空间的映射。注意到一个别名可能对应多个命名空间,如app别名对应四个命名空间——所以,这里有个疑问是,以后当我要使用app代表一个命名空间的时候,它怎么指向我想要的哪一个呢。

小结

初始化的成果如下:
file

本作品采用《CC 协议》,转载必须注明作者和本文链接
Was mich nicht umbringt, macht mich stärker
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 6

回答几个问题:

  1. instance方法的执行过程还是比较不好理解,绕来绕去的,看不明白为什么要bound、rebound、unset等......
    一眼看过去是觉得没必要,因为请求进来才初始化,那些东西当然就没有初始化啦。但是,我们这样的认识是基于fpm的运行模型,每一个请求都是独立,请求完毕之后php程序自动销毁。然而,要是你的PHP程序是常驻内存呢,比如swoole应用,那么就不是一回事了。

  2. 关于无限嵌套
    其实就是下面这两句代码的效果,把自身对象绑定到自身的一个变量中,相当于自身变量保存自身的一个引用,所以看起来像无限嵌套,其实在内存中只有一个对象

    static::setInstance($this);
    $this->instance('app', $this);

    这其实是一种软件设计的方法,你要是问我为什么我在GoF中没有找到这种模式啊,我只能说设计模型不是一种成文的知识体系,都是经验之谈而已,是前辈们在软件设计开发中的血泪教训总结出来的,并不是“银弹”。
    其实你在第一次写单例的时候不也写过类似的代码吗?

    class Singleton {
    private static $instance = null;
    public static function getInstance() {
        if (null === static::$instance) {
            static::$instance = new Singleton ();
        }
        return static::$instance;
    }
    private __construct() {}
    private __clone() {}
    }
  3. 对于 app 这个别名,当你使用app('app')获取容器实例时,已经不会走别名解析,还是上面说到的

    $this->instance('app', $this);

    已经绑定到instances数组的实例,就会直接取,所以app('app')获取到的就是Illuminate\Foundation\Application的一个实例。我们可以关注其它,比如app('queue'),我们知道

    public function registerCoreContainerAliases()
    {
    foreach ([
        ...
        'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
        ...
    ] as $key => $aliases) {
        foreach ($aliases as $alias) {
            $this->alias($key, $alias);
        }
    }
    }

    queue 绑定三个命名空间,当我们 app('queue') 时,

    protected function resolve($abstract, $parameters = [])
    {
        $abstract = $this->getAlias($abstract);
    
        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
    
       ...
       $concrete = $this->getConcrete($abstract);
    
       ...
    }

    由于'queue'就是别名getAlias()会直接返回,我们看getConcrete(),里面调用getContextualConcrete($abstract),继续看可以看到,所以清楚了吧?

    foreach ($this->abstractAliases[$abstract] as $alias) {
    if (! is_null($binding = $this->findInContextualBindings($alias))) {
        return $binding;
    }
    }

    其中比较绕的是关于容器如何去实例化一个对象,中间有很多寻找对象类的步骤。

5年前 评论

写的很不错,期待后续好文,共同加油。

5年前 评论

多个命名空间,composer似乎生成了映射文件。。

5年前 评论

回答几个问题:

  1. instance方法的执行过程还是比较不好理解,绕来绕去的,看不明白为什么要bound、rebound、unset等......
    一眼看过去是觉得没必要,因为请求进来才初始化,那些东西当然就没有初始化啦。但是,我们这样的认识是基于fpm的运行模型,每一个请求都是独立,请求完毕之后php程序自动销毁。然而,要是你的PHP程序是常驻内存呢,比如swoole应用,那么就不是一回事了。

  2. 关于无限嵌套
    其实就是下面这两句代码的效果,把自身对象绑定到自身的一个变量中,相当于自身变量保存自身的一个引用,所以看起来像无限嵌套,其实在内存中只有一个对象

    static::setInstance($this);
    $this->instance('app', $this);

    这其实是一种软件设计的方法,你要是问我为什么我在GoF中没有找到这种模式啊,我只能说设计模型不是一种成文的知识体系,都是经验之谈而已,是前辈们在软件设计开发中的血泪教训总结出来的,并不是“银弹”。
    其实你在第一次写单例的时候不也写过类似的代码吗?

    class Singleton {
    private static $instance = null;
    public static function getInstance() {
        if (null === static::$instance) {
            static::$instance = new Singleton ();
        }
        return static::$instance;
    }
    private __construct() {}
    private __clone() {}
    }
  3. 对于 app 这个别名,当你使用app('app')获取容器实例时,已经不会走别名解析,还是上面说到的

    $this->instance('app', $this);

    已经绑定到instances数组的实例,就会直接取,所以app('app')获取到的就是Illuminate\Foundation\Application的一个实例。我们可以关注其它,比如app('queue'),我们知道

    public function registerCoreContainerAliases()
    {
    foreach ([
        ...
        'queue'                => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
        ...
    ] as $key => $aliases) {
        foreach ($aliases as $alias) {
            $this->alias($key, $alias);
        }
    }
    }

    queue 绑定三个命名空间,当我们 app('queue') 时,

    protected function resolve($abstract, $parameters = [])
    {
        $abstract = $this->getAlias($abstract);
    
        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
    
       ...
       $concrete = $this->getConcrete($abstract);
    
       ...
    }

    由于'queue'就是别名getAlias()会直接返回,我们看getConcrete(),里面调用getContextualConcrete($abstract),继续看可以看到,所以清楚了吧?

    foreach ($this->abstractAliases[$abstract] as $alias) {
    if (! is_null($binding = $this->findInContextualBindings($alias))) {
        return $binding;
    }
    }

    其中比较绕的是关于容器如何去实例化一个对象,中间有很多寻找对象类的步骤。

5年前 评论

@JimChen 感谢详细的解答,说的很清楚了:+1:

5年前 评论

不错哦,小lala :kissing_heart: :kissing_heart: :heart_eyes: :heart_eyes:

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!