深入理解laravel服务容器

深入理解laravel服务容器

简介

服务容器的核心是管理类依赖和执行依赖注入的工具,
是laravel中最核心的概念之一,充分理解有助于我们更好的使用框架进行开发,这篇文章将深入剖析源码解开容器的神秘面纱。

准备

  • 依赖注入

    依赖注入即是将类中所需要用到的依赖,通过外部注入到类中进行实例化的过程。

    通俗的讲,以往我们在类A中实例化另外一个类B的过程,转移到类外进行,从而将已经实例化的对象B当做参数传递至A中,此过程就完成了依赖注入。

  • 控制反转

    控制反转即是将类中所需依赖的控制权从类内部转移至外部,由外部决定依赖的对象,他们之间的关系更像是:控制反转是「目的」,而依赖注入是「方法」,通过依赖注入的方式实现了控制反转的目的。

  • 依赖注入容器

    往往我们在使用依赖注入时,并不会单独的对依赖进行实例化, 而是通过一个专门负责管理实例化类的模块来进行,这就是依赖注入容器也就是我们本章的主题-服务容器,而在使用容器时我们不需要一个一个的使用关键字new去实例化所需的依赖,一切都交给容器来自动进行,这正是「laravel服务容器」的强大之处。

图解

image

源码分析

1.入口文件

index.php:

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

//这里就是核心,加载服务容器
$app = require_once __DIR__.'/../bootstrap/app.php';

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

2.容器初始化

bootstrap/app.php:

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

//下面几个就是加载框架基础服务
$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
);

3.注册容器

3.1构造方法里主要注册了容器启动的一些和用户自定义以及业务无关的基础服务

Application.php:

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    //注册基础绑定实例
    $this->registerBaseBindings();

    //注册服务提供者
    $this->registerBaseServiceProviders();

    //注册别名,这个方法太长就不贴出来了,逻辑相对简单大家一看应该就了解
    $this->registerCoreContainerAliases();
}

//主要是将容器本身注册,这样就方便我们在其他服务提供者中使用容器对象
protected function registerBaseBindings()
{
    static::setInstance($this);

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

    $this->instance(Container::class, $this);

    $this->instance(PackageManifest::class, new PackageManifest(
        new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
    ));
}

//这里主要注册一些基础的服务提供者:事件处理、日志、路由相关
protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));

    $this->register(new LogServiceProvider($this));

    $this->register(new RoutingServiceProvider($this));
}

3.2下面我们来着重看下register方法,看他是如何将服务注册进容器中的。

Application.php:

//第一个参数provider 是想要注册的服务,第二个参数是注册服务需要用到的参数, 第三个参数表示是否每次都重新注册
public function register($provider, $options = [], $force = false)
{
    //这里校验如果服务提供已经注册,就不重新注册了
    if (($registered = $this->getProvider($provider)) && ! $force) {
        return $registered;
    }


    if (is_string($provider)) {
        $provider = $this->resolveProvider($provider);
    }

    //这里是重点, 如果给定的类中存在register方法,那么就调用他,框架初始化中也是使用这种方法。
    if (method_exists($provider, 'register')) {
        $provider->register();
    }

    //如果服务提供者中有bindings属性,则将bindings中的对象一一注册,例如:protected $bindings = [Cache::class => function($app){ return new Cache();}];
    if (property_exists($provider, 'bindings')) {
        foreach ($provider->bindings as $key => $value) {
            $this->bind($key, $value);
        }
    }

    //和bind方法一样,只不过这里是单例
    if (property_exists($provider, 'singletons')) {
        foreach ($provider->singletons as $key => $value) {
            $this->singleton($key, $value);
        }
    }

    //对服务进行标记,下次就不再重新注册了
    $this->markAsRegistered($provider);

    //如果服务已经注册,则重新执行服务的boot方法
    if ($this->booted) {
        $this->bootProvider($provider);
    }

    return $provider;
}

//注册服务至容器中,这里说明一下,单例也是调用这个方法,只不过shared参数传true
public function bind($abstract, $concrete = null, $shared = false)
{

    //删除已绑定的实例,否则读取时会优先读取instances属性,那样的话这里的绑定就始终不会读取了
    $this->dropStaleInstances($abstract);

    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    //如果传来的实现不是一个闭包,则为其生成一个闭包
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    //将服务的名称与实现绑定至容器中
    $this->bindings[$abstract] = compact('concrete', 'shared');


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

//为实例生成一个闭包
protected function getClosure($abstract, $concrete)
{
    //这里的意思是如果抽象名称和实现一样的话,则利用反射解析具体实现类
    //如果不一样的话则实例化传来的实现类,这里如果$concrete是一个接口的话,需要提前注册至容器中,否则无法实例化。
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }

        return $container->make($concrete, $parameters);
    };
}
3.3这里我们仅拿出部分Provider进行展示一下
class EventServiceProvider extends ServiceProvider
{

    //可以看到这个类中只有一个register方法, 作用就是注册events服务
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

class RoutingServiceProvider extends ServiceProvider
{

    //register方法中提供了一系列的注册服务
    public function register()
    {
        $this->registerRouter();
        $this->registerUrlGenerator();
        $this->registerRedirector();
        $this->registerPsrRequest();
        $this->registerPsrResponse();
        $this->registerResponseFactory();
        $this->registerControllerDispatcher();
    }

    /**
     * Register the router instance.
     *
     * @return void
     */
    protected function registerRouter()
    {
        $this->app->singleton('router', function ($app) {
            return new Router($app['events'], $app);
        });
    }

}

这里需要说明一下,大部分的provider方法里只会有register和boot方法,框架执行的顺序是先执行所有服务提供者的register方法,然后再依次执行boot方法,所以这也就为什么框架中建议我们在register方法中不要调取其他服务的原因。

3.4待容器初始化完成后,注册几个我们应用的基础服务

bootstrap/app.php:

//处理我们Http请求
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

//处理控制台请求的
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

//处理debug的
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

至此容器的注册绑定已经完成(其中各服务内部处理没有讲解,只关注重点部分),下面就是容器的解析(make)部分

4.容器使用

4.1容器的使用只有几行代码,内部处理了我们所有的http请求。

index.php:

//入口文件中只make了一个Illuminate\Contracts\Http\Kernel::class服务, 还记得我们是在哪里注册他的吗?
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

//这里就是用我们解析出来的对象去处理http请求了,暂时不深入去看他了。
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
4.2下面我们着重看一下make方法

Container.php:


//make方法底层最终调用Container类中的resolve方法,中间做了下别名相关的处理,这里就不一层一层展示了,我们直接看底层方法。
protected function resolve($abstract, $parameters = [])
{
    //获取要解析的服务别名
    $abstract = $this->getAlias($abstract);

    //是否需要上下文参数绑定。
    //这里给我感觉好像是将传来的参数设置至容器的上下文参数中,等后续实例化需要的时候直接拿来用。
    //这个解释不一定对,对这里理解的不够深入,有了解这块的同学可以分享一下
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    //如果此服务已经绑定至实例中,就直接拿来用了。
    //注意instances里存的都是已经实例化过的对象,可以直接使用,这也就是为什么在bind方法中要先删除实例的原因。
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    //将传来的参数存储起来,在build方法中返回闭包时再传入
    $this->with[] = $parameters;

    //获取此服务的具体实现
    $concrete = $this->getConcrete($abstract);

    //这里就是make方法的重点了
    //先判断此实现是否可以构建,如果可以构建,则构建(实例化).
    //如果不可以则再次调用自己,将实现当做抽象名称传入,然后一层一层的解析出来。
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    //这里暂时不知道是干啥用的
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    //这里如果此服务是单例,则将解析出来的对象存至instances属性中,下次再make时就直接使用了。
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

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

    $this->resolved[$abstract] = true;

    array_pop($this->with);

    //返回实现对象
    return $object;
}

//解析实现,最终返回实例化的对象
public function build($concrete)
{
    //如果实现本身就是一个闭包,则直接返回
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    //实例化一个反射对象
    $reflector = new ReflectionClass($concrete);

    //如果类无法实例化,抛出异常(比如实现是一个接口类)
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    //获取构造方法
    $constructor = $reflector->getConstructor();

    //如果类中没有构造方法,则直接实例化后返回对象。
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    //获取构造方法中的参数(依赖)
    $dependencies = $constructor->getParameters();

    //遍历解决依赖
    $instances = $this->resolveDependencies(
        $dependencies
    );

    array_pop($this->buildStack);

    //用给定的参数实例化类
    return $reflector->newInstanceArgs($instances);
}

//循环解析依赖
protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        //没太明白这里是干啥用的,但好像绝大部分都没有走到这里,有了解的同学可以分享一下。
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        //判断如果参数是一个类,则解析类
        //如果不是类,则从上下文参数中获取对应参数或者使用默认值。(如果上下文和默认值都没有,就抛出异常,无法实例化)
        $results[] = is_null($dependency->getClass())
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);
    }

    return $results;
}

//解析依赖类
protected function resolveClass(ReflectionParameter $parameter)
{
    try {
        //可以看到这里其实最终调用的是容器中的make方法来获取某个类的具体实现
        //也就是说依靠反射来解析的类依赖,需要提前绑定至容器中,否则就无法实例化。
        return $this->make($parameter->getClass()->name);
    }

    catch (BindingResolutionException $e) {
        //如果是可选的就返回默认值
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        throw $e;
    }
}

至此,laravel容器的注册与获取已经分析完成,其实容器本身不难理解,只不过laravel中做了一些其他的处理,例如:别名、实例、单例、反射等,就是为了提升性能以及方便我们使用,所以大家可以深入了解一下。

我在这个过程中基本只关注了容器本身的实现流程,对服务中定义实现代码没有太过深入阅读,有对这方面感兴趣的同学,可以阅读官方出的「laravel生命周期」等系列文章。

网上虽然已经有很多类似的文章,但阅读源码后还是尽可能的输出一下,方便自己加深理解的同事也有可能帮助到其他人,其中对流程中的关键代码注解了一些自己的理解,可能不一定正确,如果有不对的地方欢迎大家指出。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2
wangchunbo

写的很好啊,支持一下

4年前 评论
CrazyZard

写的很强了

4年前 评论

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