Laravel的Ioc容器

firefule發表於2021-09-09

引言

學習laravel而不瞭解容器的知識,那談不上會laravel。本文從一個laravel的初學者角度,一步一步瞭解容器是在什麼樣的場景下產生的,以及laravel中是如何使用容器的。在看本文之前,如果有反射和匿名函式的基礎,會更容易理解。

一、控制反轉(Ioc)、依賴注入(DI)

學習laravel的容器,首先需要了解,依賴注入(Dependency Injection)和控制反轉(Inversion of Control)這兩個概念,以及他們的關係。

依賴注入是控制反轉的一種實現方式

先了解一下什麼是控制反轉。當呼叫者需要被呼叫者的協助時,在傳統的程式設計過程中,通常由呼叫者來建立被呼叫者的例項,但在這裡,建立被呼叫者的工作不再由呼叫者來完成,而是將被呼叫者的建立移到呼叫者的外部,從而反轉被呼叫者的建立,消除了呼叫者對被呼叫者建立的控制,因此稱為控制反轉

上邊的文字描述有點抽象,下邊看一個場景,幫助我們瞭解控制反轉。假設使用者登入的時候,系統會提供記錄登入日誌的功能,可以選擇使用【檔案】或者【資料庫】的方式來記錄日誌。實現方案如下:

<?php
// 定義寫日誌的介面規範
interface Log
{
    public function write();
}

// 檔案記錄日誌
class FileLog implements Log
{
    public function write(){
        echo 'file log write...'.PHP_EOL;
    }
}


// 資料庫記錄日誌
class DatabaseLog implements Log
{
    public function write(){
        echo 'database log write...'.PHP_EOL;
    }
}

// 程式操作類
class User
{
    protected $fileLog;

    public function __construct()
    {
        //在呼叫者(User)中建立被呼叫者的例項
        $this->fileLog = new FileLog();
    }

    public function login()
    {
        // 登入成功,記錄登入日誌
        echo 'login success...'.PHP_EOL;
        $this->fileLog->write();
    }

}

$user = new User();
$user->login();

上邊的寫法可以實現透過檔案的方式記錄日誌,【假設現在需要透過資料庫的方式記錄日誌的話】我們就需要修改User類,這樣的話,程式碼就沒達到解耦合的目的。

要實現控制反轉,通常的解決方案是將建立被呼叫者例項的工作交由 IoC 容器來完成,然後在呼叫者中注入被呼叫者(透過構造器/方法注入實現),這樣我們就實現了呼叫者與被呼叫者的解耦,該過程被稱為依賴注入依賴注入不是目的,它是一系列工具和手段,最終的目的是幫助我們開發出鬆散耦合(loose coupled)、可維護、可測試的程式碼和程式。這條原則的做法是大家熟知的面向介面,或者說是面向抽象程式設計。

現在我們按照上邊說的【依賴注入】的方式,對User類進行修(在呼叫者中注入被呼叫者(透過構造器/方法注入實現)),這樣我們就實現了呼叫者與被呼叫者的解耦,該過程被稱為依賴注入):

class User 
{
    protected $log;
    //這樣寫其實是有問題的,因為Log是介面型別,不能被例項化,後邊會透過繫結的方式,解決這個問題
    public function __construct(Log $log)
    {
        $this->log = $log;   
    }

    public function login()
    {
        // 登入成功,記錄登入日誌
        echo 'login success...';
        $this->log->write();
    }

}

//透過這種,在呼叫者中注入被呼叫者,能幫助我們解耦
$user = new User(new DatabaseLog());
$user->login();

剛開始接觸laravel的時候,就特別好奇很多物件例項透過方法的引數定義就能傳進來,而呼叫的時候也不需要我們手動的去傳入,比如說Request,所以這個時候就需要知道larave是怎麼實現的。要想了解laravel是怎麼實現的,需要先了解php中的反射,因為laravel容器的實現藉助了反射,所以先大致介紹一下反射。

二、反射

反射的概念其實可以理解成根據類名返回該類的任何資訊,比如該類有什麼方法,引數,變數等等。反射官方文件:

就拿剛才的User類來舉例:

// 獲取User的reflectionClass物件
$class = new reflectionClass(User::class);

// 拿到User的建構函式
$constructor = $class->getConstructor();

// 拿到User的建構函式的所有依賴引數
$dependencies = $constructor->getParameters();

//建立user物件
$user = $reflector->newInstance();

// 建立user物件,需要傳遞引數的
$user = $reflector->newInstanceArgs($dependencies = []);

現在建立一個make方法,將User類的名字作為引數傳給make方法,在make中透過反射機制拿到User的建構函式,進而得到建構函式的引數物件,然後透過遞迴的方式建立引數的依賴,最後就是透過newInstanceArgs方法生成User例項:

//我們這裡需要修改一下User的建構函式,如果不去修改,反射是不能動態建立介面的,如果非要用介面,後邊會透過Ioc容器去解決
class User 
{
    protected $log;

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

    public function login()
    {
        // 登入成功,記錄登入日誌
        echo 'login success...';
        $this->log->write();
    }

}

function make($concrete){
    
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
// 為什麼這樣寫的? 主要是遞迴。比如建立FileLog不需要傳入引數。
if(is_null($constructor)) {
    return $reflector->newInstance();
}else {
// 建構函式依賴的引數
$dependencies = $constructor->getParameters();
// 根據引數返回例項,如FileLog
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
 }

}

function getDependencies($paramters) {
    $dependencies = [];
    foreach ($paramters as $paramter) {
        $dependencies[] = make($paramter->getClass()->name);
    }
    return $dependencies;
}

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

如果不熟悉反射,上邊這段程式碼可能有點難理解,但是如果啃明白了,會覺得特別有意思,成就感滿滿!上邊介紹了依賴注入、控制反轉、反射,下邊進入本文重點,Ioc容器。

三、IoC容器和服務提供者

我們上邊透過反射的方式,其實還沒有達到解耦的目的,假如現在要換別的方式記錄日誌,還是需要修改User。現在我們就藉助容器來實現真正的解耦。

先借助一個容器,提前將log、user都繫結到Ioc容器中。然後User的建立就交給容器去做。

實現思路:
1、IoC容器維護binding陣列記錄bind方法傳入的鍵值對如:log=>FileLog, user=>User。也就是說我們提前把我們需要用到的類,都繫結到這個陣列中,並給類一個別名
2、在ioc->make(‘user’)的時候,透過反射拿到User的建構函式,拿到建構函式的引數,發現引數是User的建構函式引數log,然後根據log得到FileLog。
3、這時候我們只需要透過反射機制建立 KaTeX parse error: Expected 'EOF', got '、' at position 27: …ew FileLog(); 4、̲透過newInstanceAr…filelog);

大致長下邊這樣:

//例項化ioc容器
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();

這個容器就指Ioc容器,這個User可以理解成服務提供者。上邊說到了,如果User的建構函式引數是介面該如何處理,其實就是透過Ioc容器提前繫結好。
核心實現程式碼:

interface log
{
    public function write();
}

// 檔案記錄日誌
class FileLog implements Log
{
    public function write(){
        echo 'file log write...';
    }
}

// 資料庫記錄日誌
class DatabaseLog implements Log
{
    public function write(){
        echo 'database log write...';
    }
}

class User
{
    protected $log;
    public function __construct(Log $log)
    {
        $this->log = $log;
    }
    public function login()
    {
        // 登入成功,記錄登入日誌
        echo 'login success...';
        $this->log->write();
    }
}
class Ioc
{
    public $binding = [];

    public function bind($abstract, $concrete)
    {
        //這裡為什麼要返回一個closure呢?因為bind的時候還不需要建立User物件,所以採用closure等make的時候再建立FileLog;
        $this->binding[$abstract]['concrete'] = function ($ioc) use ($concrete) {
            return $ioc->build($concrete);
        };

    }

    public function make($abstract)
    {
    	// 根據key獲取binding的值
        $concrete = $this->binding[$abstract]['concrete'];
        return $concrete($this);
    }

    // 建立物件
    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);
        }
    }

    // 獲取引數的依賴
    protected function getDependencies($paramters) {
        $dependencies = [];
        foreach ($paramters as $paramter) {
            $dependencies[] = $this->make($paramter->getClass()->name);
        }
        return $dependencies;
    }

}

//例項化IoC容器
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();

現在不需要關心是用什麼方式記錄日誌了,哪怕後期需要修改記錄日誌的方式,只需要在ioc容器修改繫結其他記錄方式日誌就行了。

那麼laravel中的服務容器和服務提供者長啥樣呢?
可以在config目錄找到app.php中providers,這個陣列定義的都是已經寫好的服務提供者

$providers = [
    IlluminateAuthAuthServiceProvider::class,
    IlluminateBroadcastingBroadcastServiceProvider::class,
    IlluminateBusBusServiceProvider::class,
    IlluminateCacheCacheServiceProvider::class,
    ...
]
...
// 隨便開啟一個類比如CacheServiceProvider,這個服務提供者都是透過呼叫register方法註冊到ioc容器中,其中的app就是Ioc容器。singleton可以理解成我們的上面例子中的bind方法。只不過這裡singleton指的是單例模式。

class CacheServiceProvider{
    public function register()
    {
        $this->app->singleton('cache', function ($app) {
            return new CacheManager($app);
        });

        $this->app->singleton('cache.store', function ($app) {
            return $app['cache']->driver();
        });

        $this->app->singleton('memcached.connector', function () {
            return new MemcachedConnector;
        });
    }
}

具體服務提供者register方法是什麼時候執行的,後邊會大致說一下Laravel的生命週期

三、Facade外觀模式的原理

我們經常會在laravel中透過這樣的方式來呼叫方法:
User::query()->where()
這種寫法要比我們剛才需要先透過KaTeX parse error: Expected 'EOF', got '拿' at position 18: …c->make('user')拿̲到User的例項,然後再使用user->login()。

那上邊那種簡單的方式是如何實現的呢?
Facade的工作原理:
1、定義一個服務提供者的外觀類,在該類中定義一個容器類的變數,跟ioc容器繫結的key一樣,
2、透過靜態魔術方法__callStatic可以得到當前想要呼叫的login
3、使用static::$ioc->make(‘user’);

現在透過這種外觀類的方式去修改一下我們上邊的那個記錄日誌的類(即給User類寫一個外觀類):

class UserFacade
{
    // 維護Ioc容器
    protected static $ioc;

    public static function setFacadeIoc($ioc)
    {
        static::$ioc = $ioc;
    }

    // 返回User在Ioc中的bind的key
    protected static function getFacadeAccessor()
    {
        return 'user';
    }

    // php 魔術方法,當靜態方法被呼叫時會被觸發
    public static function __callStatic($method, $args)
    {
        $instance = static::$ioc->make(static::getFacadeAccessor());
        return $instance->$method(...$args);
    }

}

$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');

UserFacade::setFacadeIoc($ioc);

UserFacade::login();

可能大家感覺加了這個User的外觀類更加麻煩了,需要注入容器,還需要使用魔術方法,其實laravel在執行的時候都將這些工作做好了。我們直接使用UserFacade::login()就可以了。最主要的就是Facade提供了簡單易記的語法,從而無需配置長長的類名。像laravel中的Redis、Log等都用的是這種外觀模式。

四、Laravel的生命週期

要研究laravel的生命週期,肯定是要看入口檔案的

// 定義了laravel一個請求的開始時間
define('LARAVEL_START', microtime(true));

// composer自動載入機制
require __DIR__.'/../vendor/autoload.php';

//這句話你就可以理解laravel,在最開始引入了一個ioc容器。
$app = require_once __DIR__.'/../bootstrap/app.php';

開啟__DIR__.'/../bootstrap/app.php';你會發現這段程式碼,繫結了IlluminateContractsHttpKernel::class,這個你可以理解成之前我們所說的$ioc->bind();方法。

// 這個相當於我們建立了Kernel::class的服務提供者
$kernel = $app->make(IlluminateContractsHttpKernel::class);

// 獲取一個 Request ,返回一個 Response。以把該核心想象作一個代表整個應用的大黑盒子,輸入 HTTP 請求,返回 HTTP響應。
$response = $kernel->handle(
$request = IlluminateHttpRequest::capture()
);

// 就是把我們伺服器的結果返回給瀏覽器。
$response->send();

// 這個就是執行我們比較耗時的請求,
$kernel->terminate($request, $response);

上邊其實還是比較抽象的,在網上看見一張把laravel的生命週期畫的非常清楚的圖,分享給大家:
圖片描述
我覺得了解了在laravel中容器是個什麼之後,對後邊更深入的學習laravel是非常有幫助的,希望大家看完之後能真正的有所收穫!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1834/viewspace-2824577/,如需轉載,請註明出處,否則將追究法律責任。

相關文章