Laravel源碼分析之服務容器和服務提供者 源碼解讀 和 實現(完整代碼)

引文

  • 服務容器是用於管理類的依賴和執行依賴注入的工具。
  • 服務提供者是Laravel應用程序的引導中心,核心服務通過服務提供者進行註冊,如服務容器容器綁定,中間件等服務提供器。Laravel項目配置文件config/app.php中有個providers數組,數組中的內容即是應用程序需要加載的服務提供器。
    在這裏插入圖片描述
  • 接上篇文章Laravel源碼分析之控制反轉和依賴注入, 在這篇文章中, 已經實現了一個簡單的IOC容器,但是還是不能很好的解耦,如圖上文代碼:
/**
 * 日誌接口
 */
interface Log
{
    public function log($msg);
}

/**
 * 文件日誌的實現
 */
class FileLog implements Log
{
    /**
     * 將log進行一個簡單的輸出
     * @param $msg
     * @return Log|void
     */
    public function log($msg)
    {
        echo '文件日誌記錄: ' . $msg . PHP_EOL;
    }
}


/**
 * 數據庫日誌的實現
 */
class DbLog implements Log
{
    public function log($msg)
    {
        echo '數據庫日誌記錄:' . $msg . PHP_EOL;
    }
}


class User
{
    private $log;
    /**
     * @param FileLog $log
     */
    public function __construct(FileLog $log)
    {
        $this->log = $log;
    }

    /**
     * 簡單的登錄操作
     * @param string $username
     */
    public function login($username='ClassmateLin')
    {
        echo '用戶:' . $username . '登錄成功!' . PHP_EOL;
        $this->log->log('日誌: 用戶:' . $username . '登錄成功!');
    }
}


class Application
{

    function make(string $class_name)
    {

        $reflector = new reflectionClass($class_name); // 拿到反射實例
        $constructor = $reflector->getConstructor(); // 拿到構造函數

        if (is_null($constructor)) { // 如果寫構造函數,得到的constructor是null。
            return $reflector->newInstance(); // 進行無參數實例化
        }

        // 拿到構造函數依賴的參數
        $dependencies = $constructor->getParameters();

        // 這時候我們依賴的參數可能也有參數,通過遞歸的去獲取當前類的參數。
        $instance = $this->getDependencies($dependencies);

        // 進行帶參數的實例化
        return $reflector->newInstanceArgs($instance);

    }

    /**獲取依賴
     * @param $params
     * @return array
     */
    private function getDependencies($params)
    {
        $dependencies = [];
        // array_walk相等於foreach, for 的作用,據說速度是最快的,我也沒去驗證,只是喜歡閉包。
        array_walk($params, function ($param) use (&$dependencies) {
            $class_name = $param->getClass()->name;  // 獲取類名
            $dependencies[] = $this->make($class_name);  // 調用make函數創建實例
        });
        return $dependencies;
    }
}

$app = new Application();
$user = $app->make('User');
$user->login();

雖說已經實現了一個簡單的容器,但是並不能很好的解耦,User 依賴的是具體類,而不是依賴於抽象接口。
當我們想將FileLog修改爲DbLog時,如果只有一兩個類使用了FileLog那麼還有,當然日誌不可能僅在一兩個類中使用,非常繁瑣,所以需要實現在配置文件中,通過修改log=database或者log=file來進行配置日誌。
此時我們需要藉助一個容器來實現綁定,而此時類應該依賴於抽象,不應該再依賴於實體。

  • 接下來,一步步的瞭解Laravel服務容器的實現,當然進行了簡化。

容器代碼閱讀

源碼閱讀

  • 在入口文件public/index.php有一行代碼是這樣寫的: $app = require_once __DIR__.'/../bootstrap/app.php';,包含了bootstrap/app.php文件, $app這個變量可以理解爲是容器。
  • bootstrap/app.php代碼定義如下:
<?php
# 實例化了一個Application類,並將跟目錄傳進去了。
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

# 註冊單例對象
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

// 省略類似代碼

# 返回核心應用(容器)
return $app;
  • 接着鼠標移動到Application上按CTRL+B,跳轉到Application的構造方法中, 其內容如下:
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath); // 設置路徑
        }

        $this->registerBaseBindings();  // 註冊基本內容

        $this->registerBaseServiceProviders();  // 註冊基本服務提供者
 
        $this->registerCoreContainerAliases(); // 註冊核心容器別名
    }

構造方法主要是對項目路徑的綁定,註冊基本應用,註冊基本服務提供者,註冊容器核心別名,讀者可自行點進去看源碼,現在主要來看下最後一個方法做了什麼, 代碼如下:

    public function registerCoreContainerAliases()
    {
        foreach ([
            'app'                  => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class,  \Psr\Container\ContainerInterface::class],
		// 省略了一部分
        'log'                  => [\Illuminate\Log\Writer::class, \Illuminate\Contracts\Logging\Log::class, \Psr\Log\LoggerInterface::class],
         'redis'                => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class],
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

可以看到foreach中是一個關聯數組, 如鍵log中對應也是一個數組。通過迭代獲取每個鍵和值,然後執行了, this->alias方法。alias方法中只有簡單的兩行:

    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $abstract; // 鍵值對換存儲
        $this->abstractAliases[$abstract][] = $alias; // 這裏相等於把原來的數組複製了一份。
    }

如果源碼看的不清楚,可以用以下代碼執行打印出來看看:

<?php
namespace IOC;

interface LogInterface{}

class LogWrite{}


class Application
{

    private $aliases = [];
    private $abstractAliases = [];

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

    public function registerCoreContainerAliases()
    {
        foreach ([
                     'log' => [LogWrite::class, LogInterface::class]
                 ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }

    public function alias($abstract, $alias)
    {
        $this->aliases[$alias] = $abstract; // 鍵 值 對調換
        $this->abstractAliases[$abstract][] = $alias; // 將值添加到對應的鍵的數組中。
    }
    
    public function print()
    {
        print_r($this->aliases);
        print_r($this->abstractAliases);
    }
}

$app = new Application();
$app->print();
  • 接下來看bootstrap/app.php中的代碼:
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);
  • singleton中實際上調用了bind方法, 是對bind方法的一層封裝,bind方法中參數有三個, 依次是$abstract(屬性名), $concrete(閉包函數), $share是否共享,該對象用於標識全局是否只有一個實例。
    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true); // bind一個單例.
    }
  • bind方法源碼如下:
    public function bind($abstract, $concrete = null, $shared = false)
    {
        $this->dropStaleInstances($abstract);
        /*先刪除已有的實例, unset($this->instances[$abstract], $this->aliases[$abstract]);
		這裏終於見到了上面提到的$this->aliases數組了。
		*/

        if (is_null($concrete)) {  // 如果沒有閉包參數
            $concrete = $abstract;  // 則把當前閉包設置爲屬性,下面綁定前會先判斷是否爲閉包,否則去獲取閉包
        }
        
        if (! $concrete instanceof Closure) {
        	// 如果不是閉包則去獲取一個閉包
            $concrete = $this->getClosure($abstract, $concrete);
        }
		// compact() 在當前的符號表中查找該變量名並將它添加到輸出的數組中,變量名成爲鍵名而變量的內容成爲該鍵的值
		// 添加到bindings數組中
        $this->bindings[$abstract] = compact('concrete', 'shared');
        
        if ($this->resolved($abstract)) {// 如果這個屬性已經實例化了,那麼會重新實例化
            $this->rebound($abstract);  // 到這裏才進行實例化
        }
    }
  • 主要看看rebound方法:
    protected function rebound($abstract)
    {
        $instance = $this->make($abstract);
        // 獲取當前abstract rebound操作需要調用方法
        foreach ($this->getReboundCallbacks($abstract) as $callback) {
            call_user_func($callback, $this, $instance); // 調用函數, 第一個爲回調函數,後面爲參數。
        }
    }
  • 接着看make方法:
public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

在make中調用了resolve,有兩個參數,第一個參數爲屬性,第二個爲參數, resolve代碼如下:

    protected function resolve($abstract, $parameters = [])
    {	
    	
    	// 這裏出現了前面提到的alias數組,這裏正是通過log去拿到了LogWrite:class的操作。
        $abstract = $this->getAlias($abstract);
		
		// 判斷參數是否爲空,或者有沒有上下文綁定
        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
		// 如果已經有該實例了且不需要上下文綁定,那麼直接可以返回實例了,也表示單例
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];  
        }

        $this->with[] = $parameters;
		// 拿到其閉包
        $concrete = $this->getConcrete($abstract);
		// isBuildabe方法通過$concrete === $abstract || $concrete instanceof Closure 來判斷是否可以實例化;
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete); // 可以實例化則調用build函數
        } else {
            $object = $this->make($concrete); // 否則遞歸調用make方法。
        }
        
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

 		// 判斷是否單例或者不需要上下文
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object; // 直接將實例保存
        }
		// 進行需要回調的函數調用
        $this->fireResolvingCallbacks($abstract, $object);
        
        $this->resolved[$abstract] = true;  // 標識爲已經註冊過的屬性
        array_pop($this->with);
        return $object;  // 最後返回一個實例對象
    }
  • 接下來主要看看build方法幹了什麼, 其代碼:

    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);
    }

我上篇文章Laravel源碼分析之控制反轉和依賴注入中實現的其實就是這部分內容的簡化操作,只不過這裏是閉包,而我的示例中是類,都是通過反射拿構造函數,通過構造函數拿參數,如果參數也需要參數,那麼遞歸調用,直到沒有定義構造函數爲止即結束遞歸。

總結

  • IOC容器記錄鍵值和類或抽象類或閉包對應的數組,來實現容器綁定。
  • 在進行bind的時候並不會創建實例,在build的時候才創建實例。
  • 在實例化的時候通過屬性去實例化, 如屬性log, 這時候通過log去拿到對應的類。
  • 然後有判斷是否爲全局對象或者已經實例化可以直接返回,又或者需要進行清除。
  • 後面遞歸地通過反射拿到構造函數,再通過構造函數拿到其參數,直到最後創建好一個對象返回。

ICO容器的實現

思路

以上篇文章用戶和日誌的例子來思考:

  • 創建一個數組來進行綁定: [‘log’=> FileLog, ‘user’=> User]
  • 在進行ioc->make(‘user’) 的時候再通過user去拿User
  • 拿到User之後,通過反射來創建實例。

代碼

<?php


/**
 * 日誌抽象接口
 */
interface Log
{
    public function write();  // 子類需要實現一個write方法
}

/**
 * 文件日誌實現
 * @package IOC
 */
class FileLog implements Log
{
    public function write()
    {
        echo '日誌驅動: 文件' . PHP_EOL;
    }
}

/**
 * 數據庫日誌實現
 * @package IOC
 */
class DatabaseLog implements Log
{
    public function write()
    {
        echo '日誌驅動: 數據庫' . PHP_EOL;
    }
}

/**
 * 用戶類
 * @package IOC
 */
class User
{
    protected $log;

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

    /**
     * 用戶登錄成功並記錄日誌
     */
    public function login()
    {
        echo '用戶登錄成功...';
        $this->log->write();
    }
}

/**
 * IOC容器的實現
 * @package ClassmateLin
 */
class Ioc
{
    // 保存綁定的屬性
    public $bindings = [];

    /**
     * 屬性進行綁定
     * @param $abstract
     * @param $concrete
     */
    public function bind($abstract, $concrete)
    {
        // 綁定的時候還不需要創建對象, 只有在我們調用make的時候才需要創建對象,可以節省內存, 所以綁定一個閉包。
        $this->bindings[$abstract]['concrete'] = function ($ioc) use ($concrete) {
                return $ioc->build($concrete);
        };

    }

    /**
     * 創建對象
     * @param $abstract
     * @return mixed
     */
    public function make($abstract)
    {
        $concrete = $this->bindings[$abstract]['concrete']; // 根據屬性獲取其閉包
        return $concrete($this);  // 上面定義的閉包函數參數是ioc,也就是需要將類實例本身傳遞進去, 閉包函數內部調用了build方法。
    }

    // 創建對象
    public function build($concrete) {
        $reflector = new ReflectionClass($concrete); 
        $constructor = $reflector->getConstructor();  // 通過反射拿構造函數
        if(is_null($constructor)) { // 如果沒有構造函數,直接返回一個實例對象
            return $reflector->newInstance();
        }else {
            $dependencies = $constructor->getParameters(); // 通過構造函數拿參數
            $instances = $this->getDependencies($dependencies); // 解決參數的依賴,也就是遞歸的進行實例化
            return $reflector->newInstanceArgs($instances); // 通過帶參數的反射接口創建一個實例
        }
    }

    /**
     * 獲取函數依賴
     * @param $params
     * @return array
     */
    protected function getDependencies($params) {
        $dependencies = [];
        foreach ($params as $param) {
            # $param->getClass()->name可以拿到類名
            $dependencies[] = $this->make($param->getClass()->name);
        }
        return $dependencies;
    }

}

//實例化IoC容器
$ioc = new Ioc();
$ioc->bind('Log','FileLog'); // 綁定日誌
$ioc->bind('user','User'); // 綁定用戶
$user = $ioc->make('user'); // 創建用戶
$user->login();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章