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

相關文章
相關標籤/搜索