Laravel学习笔记之IoC Container实例化源码解析

说明:本文主要学习Laravel容器的实例化过程,主要包括Register Base Bindings, Register Base Service Providers , Register Core Container Aliases and Set the Base Path等四个过程。同时并把本身的一点研究心得分享出来,但愿对别人有所帮助。php

开发环境:Laravel5.3 + PHP7 + OS X10.11laravel

Laravel的入口文件是public/index.php文件,首先第一步加载composer的autoload文件:redis

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

关于composer自动加载原理可看这篇文章:Laravel学习笔记之Composer自动加载bootstrap

而后开始实例化Application容器获得全局变量$app:segmentfault

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

输入的是project的根路径,研究下\Illuminate\Foundation\Application的构造函数源码:api

public function __construct($basePath = null)
    {
        $this->registerBaseBindings();

        $this->registerBaseServiceProviders();

        $this->registerCoreContainerAliases();

        if ($basePath) {
            $this->setBasePath($basePath);
        }
    }

Create Application过程当中作了4件事:数组

1. register base bindings.

 2. register base service providers(\Illuminate\Events\EventServiceProvider and \Illuminate\Routing\RoutingServiceProvider).

 3. register core service aliases 
('app', 'auth', 'auth.driver', 'blade.compiler', 'cache', 'cache.store', 'config', 'cookie', 'encrypter', 'db', 'db.connection', 
'events', 'files', 'filesystem', 'filesystem.disk', 'filesystem.cloud', 'hash', 'translator', 'log', 'mailer', 
'auth.password', 'auth.password.broker', 'queue', 'queue.connection', 'queue.failer', 'redirect', 'redis', 'request', 
'router', 'session', 'session.store', 'url', 'validator', 'view'), and these core service will be registered later.

 4. set the base path, including 
'path' = __DIR__ . '/app', 'path.base' = __DIR__ , 'path.lang' = __DIR__ . '/resources/lang',
'path.config' = __DIR__ . '/config', 'path.public' = __DIR__ . '/public', 'path.storage' = __DIR__ . '/storage', 
'path.database' = __DIR__ . '/database', 'path.resources' = __DIR__ . '/resources', 
'path.bootstrap' = __DIR__ . '/bootstrap'. U can get theses path everywhere in the way, 
e.g.  public_path('/js/app.js') === __DIR__ . '/public/js/app.js';

1. Register Base Bindings

基础绑定主要是绑定当前Application对象进容器,绑定的是同一对象,但给了两个名字:cookie

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

$this->instance('Illuminate\Container\Container', $this);

OK, 那instance()是如何绑定服务的?
\Illuminate\Foundation\Application是extends from the \Illuminate\Container\Container,看instance()源码:session

/**
     * Register an existing instance as shared in the container.
     *
     * @param  string  $abstract
     * @param  mixed   $instance
     * @return void
     */
    public function instance($abstract, $instance)
    {
        // $abstract若是是string,截取右边的'\', 如\Illuminate\Foundation\Application => Illuminate\Foundation\Application
        $abstract = $this->normalize($abstract);
        
        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        unset($this->aliases[$abstract]);

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

        $this->instances[$abstract] = $instance;

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

分解代码,看别名的注册:app

if (is_array($abstract)) {
        list($abstract, $alias) = $this->extractAlias($abstract);

        $this->alias($abstract, $alias);
    }
    
        ...
        
    protected function extractAlias(array $definition)
    {
        return [key($definition), current($definition)];
    }   
    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $this->normalize($abstract);
    }

若是$abstract是数组, e.g. $this->instance(['app' => '\\Illuminate\\Foundation\\Application'], $this),则app是alias name,存入Container class的$aliases[ ]属性中,这样存入值是:

$aliases = [
    'app'=> '\Illuminate\Foundation\Application',
];

而后在注册到属性$instances[ ]中,则上面的绑定代码相似于;

// 这里加个别名
$this->instances['app' => '\Illuminate\Foundation\Application'] = (new \Illuminate\Foundation\Application($path = __DIR__));
$this->instances['Illuminate\Container\Container'] = (new \Illuminate\Foundation\Application($path = __DIR__));

能够PHPUnit测试下别名这个feature:

public function testAlias ()
{
    // make()是从Container中解析出service,与instance正好相反
    $object1 = App::make('app');
    $object2 = App::make('\Illuminate\Foundation\Application');
    $this->assertInstanceOf(\Illuminate\Foundation\Application::class, $object1);
    $this->assertInstanceOf(\Illuminate\Foundation\Application::class, $object2);
}

因为不是单例绑定singleton(),这里$object1与$object2都是\Illuminate\Foundation\Application的对象,但不是同一对象。singleton()和make()稍后讨论下。

同时检查下以前是否已经绑定了,若是已经绑定了,则执行以前rebinding()的回调函数,主要是执行Container的$reboundCallbacks[ ]属性值。Container提供了rebinding()函数供再一次补充绑定(如再给'app'绑定一些以前绑定没有的的行为),PHPUnit测试下:

public function testReboundCallbacks() 
{
    // Arrange
    $container = new Container;
    
    // Actual
    $container->instance('app', function(){
        return 'app1';
    });
    $a = 0
    $container->rebinding('app', function() use (&$a) {
        $a = 1;
    });
    // 再次绑定时,触发上一次rebinding中绑定该'app'的回调
    $container->instance('app', function () {
        return 'app2';
    });
    
    // Assert
    $this->assertEqual(1, $a);
}

Container的做用是供service的绑定和解析,绑定有三种方法:bind(),singleton(),instance();解析是make(),稍后讨论下容器中最重要的这几个feature。

2. Register Base Service Providers

绑定了名为'app','IlluminateContainerContainer'的两个service后(尽管绑定的service相同),看下绑定了两个基础service provider:

$this->register(new \Illuminate\Events\EventServiceProvider($this));
$this->register(new \Illuminate\Routing\RoutingServiceProvider($this));

两个基础的service provider is: IlluminateEventsEventServiceProvider和IlluminateRoutingRoutingServiceProvider。看下是如何注册两个service provider:

public function register($provider, $options = [], $force = false)
    {
        if (($registered = $this->getProvider($provider)) && ! $force) {
            return $registered;
        }
        
        if (is_string($provider)) {
            $provider = $this->resolveProviderClass($provider);
        }

        if (method_exists($provider, 'register')) {
            $provider->register();
        }
        
        foreach ($options as $key => $value) {
            $this[$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 the developer's application logics.
        if ($this->booted) {
            $this->bootProvider($provider);
        }

        return $provider;
    }

首先检查是否已经注册了,若是注册了就直接返回,主要是检查Application class 的$serviceProviders[ ]的值,看下代码:

if (($registered = $this->getProvider($provider)) && ! $force) {
        return $registered;
    }

    ...

    public function getProvider($provider)
    {
        $name = is_string($provider) ? $provider : get_class($provider);

        return Arr::first($this->serviceProviders, function ($value) use ($name) {
            return $value instanceof $name;
        });
    }

若是输入的是字符串,就直接new $provider($this)生成对象,因此上面两个注册能够这么写:

$this->register(\Illuminate\Events\EventServiceProvider::class);
$this->register(\Illuminate\Routing\RoutingServiceProvider::class);

而后执行service provider中的register()方法,稍后看下两个base service provider注册了哪些service。

而后把注册过的service provider标记为provided,就是写入到$serviceProviders[ ]中,而开始是先检查$serviceProviders[ ]中,有没有已经注册过的将要注册的service。看下markAsRegistered()源码:

protected function markAsRegistered($provider)
    {
        $this['events']->fire($class = get_class($provider), [$provider]);

        $this->serviceProviders[] = $provider;

        $this->loadedProviders[$class] = true;
    }

这里还用了刚注册的'events' service来触发该service provider已经注册的事件,并把该service provider写入到已经加载的属性中loadedProviders[ ].

而后检查程序是否已经启动,若是已经启动完成了,再执行每个service provider中的boot()方法,这里会发现为啥每个service provider里常常出现register()和boot()方法,而且register()是注册服务的,等全部服务注册完,再去boot()一些东西。固然,这里程序刚刚注册第一个EventServiceProvider,程序离彻底启动还早着呢。不过,能够先看下这里的bootProvider()方法源码:

protected function bootProvider(ServiceProvider $provider)
    {
        if (method_exists($provider, 'boot')) {
            return $this->call([$provider, 'boot']);
        }
    }
    /**
     * Call the given Closure / class@method and inject its dependencies.
     *
     * @param  callable|string  $callback
     * @param  array  $parameters
     * @param  string|null  $defaultMethod
     * @return mixed
     */
    public function call($callback, array $parameters = [], $defaultMethod = null)
    {
        if ($this->isCallableWithAtSign($callback) || $defaultMethod) {
            return $this->callClass($callback, $parameters, $defaultMethod);
        }

        $dependencies = $this->getMethodDependencies($callback, $parameters);

        return call_user_func_array($callback, $dependencies);
    }

重点看下call()这个Container另外一个重要的函数,若是这么调用call(EventServiceProvider@register),那就经过Container::callClass()来解析出class和method,而后在调用call(),看下callClass()源码:

protected function callClass($target, array $parameters = [], $defaultMethod = null)
    {
        $segments = explode('@', $target);
        $method = count($segments) == 2 ? $segments[1] : $defaultMethod;

        if (is_null($method)) {
            throw new InvalidArgumentException('Method not provided.');
        }

        // 而后在这样调用call([$class, $method], $parameters)
        return $this->call([$this->make($segments[0]), $method], $parameters);
    }

也就是说,若是call(EventServiceProvider@register)这种方式的话先转化成call([$class, $method], $parameters)来调用,固然要是直接这种方式就不用在转换了。这里是经过[(new EventServiceProvider($app)), 'boot']相似这种方式来调用的。在调用boot()时有依赖怎么办?使用[$class, $method]经过getMethodDependencies($parameters)来获取$dependencies,看下getMethodDependencies($parameters)源码:

protected function getMethodDependencies($callback, array $parameters = [])
    {
        $dependencies = [];

        foreach ($this->getCallReflector($callback)->getParameters() as $parameter) {
            $this->addDependencyForCallParameter($parameter, $parameters, $dependencies);
        }

        return array_merge($dependencies, $parameters);
    }
    protected function getCallReflector($callback)
    {
        if (is_string($callback) && strpos($callback, '::') !== false) {
            $callback = explode('::', $callback);
        }

        if (is_array($callback)) {
            return new ReflectionMethod($callback[0], $callback[1]);
        }

        return new ReflectionFunction($callback);
    }
    protected function addDependencyForCallParameter(ReflectionParameter $parameter, array &$parameters, &$dependencies)
    {
        if (array_key_exists($parameter->name, $parameters)) {
            $dependencies[] = $parameters[$parameter->name];

            unset($parameters[$parameter->name]);
        } elseif ($parameter->getClass()) {
            $dependencies[] = $this->make($parameter->getClass()->name);
        } elseif ($parameter->isDefaultValueAvailable()) {
            $dependencies[] = $parameter->getDefaultValue();
        }
    }

这里是经过PHP的Reflector Method来获取依赖,依赖若是是对象的话再继续make()自动解析出service,是个外部传进来的值则代入,有默认值传默认值。反射(Reflector)是PHP的一个重要的高级特性,值得研究。
总的来讲,在boot()方法中若是有dependency,container会自动解析,无论该dependency是否是某个service。这就是Method Injection,咱们知道Dependency Injection有两种:Constructor Injection and Method Injection,这里可看到Method Injection是如何实现的。

OK,而后看下两个service provider注册了些什么?
首先注册EventServiceProvider中提供的service,看有哪些:

public function register()
{
    $this->app->singleton('events', function ($app) {
        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
            return $app->make('Illuminate\Contracts\Queue\Factory');
        });
    });
}

OK,只有一个名为'events'的service注册到容器中了,而且是单例注册的。看下singleton()的源码:

public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }
    
    public function bind($abstract, $concrete = null, $shared = false)
    {
        $abstract = $this->normalize($abstract);

        $concrete = $this->normalize($concrete);

        // 若是是数组,抽取别名而且注册到$aliases[]中,上文已经讨论
        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        $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);
        }
    }
    
    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

singleton()实际上就是$shared = true 的bind()。同时舍弃掉$instances[]中已经注册过的名为$abstract的service,固然别名数组也别忘了舍弃。
若是$concrete没有提供,则使用$abstract自动补全$concrete,而且使用getClosure()封装下作个Closure:

protected function getClosure($abstract, $concrete)
    {
        // $c 就是$container,即Container Object,会在回调时传递给这个变量
        return function ($c, $parameters = []) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';

            return $c->$method($concrete, $parameters);
        };
    }

$concrete没有提供绑定的状况,如:$this->singleton(IlluminateContainerContainer::class); 只提供了$abstract.

这里,就是向$bindings[ ]中注册下,如今它的值相似这样:

$bindings = [
    'events' => [
        'concrete' => function ($app) {
                        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {return $app->make('Illuminate\Contracts\Queue\Factory');});
                      },
        'shared'   => true,
    ],
];

已经说了singleton()和binding()注册的区别就是'shared'的值不同,若是是$this->app->binding('events', Closure),则$bindings[ ]值是:

$bindings = [
    'events' => [
        'concrete' => function ($app) {
                        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {return $app->make('Illuminate\Contracts\Queue\Factory');});
                      },
        'shared'   => false,
 
    ],
 
];

OK,看下RoutingServiceProvider中注册了些什么service?
上文说过,Application中register()会调用service provider中的register()方法,看下\Illuminate\Routing\RoutingServiceProvider源码就发现其注册了几个service:'router', 'url', 'redirect', Psr\Http\Message|ServerRequestInterface::class, Psr\Http\Message\ResponseInterface::class, Illuminate\Contracts\Routing\ResponseFactory::class
只有Illuminate\Contracts\Routing\ResponseFactory::class是singleton(),其他是bind(),e.g. 'router' service source code:

$this->app['router'] = $this->app->share(function ($app) {
    return new Router($app['events'], $app);
});

为何说是bind()?而且$this->app['router']是啥意思?

OK, 看下share()的源码:

public function share(Closure $closure)
{
    return function ($container) use ($closure) {
        static $object;
 
        if (is_null($object)) {
            $object = $closure($container);
        }
 
        return $object;
    };
}

share()仅仅执行$closure()并传入$container,因此上面的'router' service代码相似于:

$this->app['router'] = new Router($app['events'], $app);

$this->app是Container对象,而Container implement ArrayAccess这个Interface,实现对类的属性作数组式访问,因此Container必须实现四个方法:

@link http://php.net/manual/en/arrayaccess.offsetset.php
public function offsetExists($offset);
public function offsetGet($offset);
public function offsetSet($offset, $value);
public function offsetUnset($offset);

这里是对$this->app赋值,因此看下offsetSet()源码:

public function offsetSet($key, $value)
{
    if (! $value instanceof Closure) {
        $value = function () use ($value) {
            return $value;
        };
    }
 
    $this->bind($key, $value);
}

这里是用bind()来绑定到container中,因此上文中说是bind(),而不是其余。所上文的代码相似于这样:

$this->app['router'] = new Router($app['events'], $app);
  
is like:
  
$object = new Router($app['events'], $app);
$this->bind('router', function () use ($object) {return $object});

总的来讲,就是经过注册EventServiceProvider and RoutingServiceProvider来绑定了一些service, e.g. 'events', 'router' and so on.

3. Register Core Container Aliases

因为PHP使用namespace来命名class,有时类名很长,因此须要作个别名alias图方便。看下registerCoreContainerAliases()的源码:

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

        foreach ($aliases as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

给class name注册个别名,而且在相同数组里有着共同的别名,e.g. 'IlluminateFoundationApplication', 'IlluminateContractsContainerContainer' and 'IlluminateContractsFoundationApplication' share the same alias name 'app'.

4. Set the Base Path

Application Constructor里须要传入一个path这个原料来构造类,这里path是这个project的当前绝对路径。同时绑定一些经常使用的文件夹路径供未来使用,看下构造函数中源码:

public function __construct($basePath)
{
      ...
      
    if ($basePath) {
        $this->setBasePath($basePath);
    }
}
public function setBasePath($basePath)
{
    $this->basePath = rtrim($basePath, '\/');
 
    $this->bindPathsInContainer();
 
    return $this;
}
protected function bindPathsInContainer()
{
    $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());
}

instance()上文已经讨论过,因此这里的$instances[ ]相似于这样:

$instances = [
    'path'           => __DIR__ . '/app',
    'path.base'      => __DIR__ . '/',
    'path.lang'      => __DIR__ . '/resources/lang',
    'path.config'    => __DIR__ . '/config',
    'path.public'    => __DIR__ . '/public',
    'path.storage'   => __DIR__ . '/storage',
    'path.database'  => __DIR__ . '/database',
    'path.resources' => __DIR__ . '/resources',
    'path.bootstrap' => __DIR__ . '/bootstrap',
];

OK,看下bootstrap/app.php文件,在获得$app这个实例化对象后,再单例绑定Two Kernel and One Exception:

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

最后,就获得一个塞满好几个service的容器了,而未被实例化前是个空Container.整个的Application的实例化过程分析就OK了。

总结:本文主要学习了Application的实例化过程,主要学习了实例化过程当中向这个IoC(Inversion of Control) Container绑定了哪些service,并讨论了绑定的三个方法:bind(),singleton(),instance(),解析方法make()留到单独研究Container时再讨论吧。下次分享下Container学习心得,并写上PHPUnit测试,到时见。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps

相关文章
相关标签/搜索