對laravel和yii服務容器的理解

在當今流行的現代化php框架中,容器的概念已經非常普及,比如yii,lavavel的設計都用到了容器。什麼是容器呢,它提供了整個框架運行過程中所需要的一系列服務,其實通俗點講,就是裝東西的盒子,框架中常見的類,對象,配置等都裝在這個盒子裏,在程序運行的過程中,動態的爲系統提供所需要的服務,這就是容器的作用。

laravel中的容器

laravel的核心其實就是一個容器,學名稱爲Ioc容器,官方文檔稱其爲“服務容器”,這裏涉及到一個重要的概念:依賴注入。你千萬不要被這名字給唬住了,我們直接來一個例子作爲說明。

Interface Power
{
    public function fight();
}

class FlyPower implements Power
{
    public function fight()
    {
        echo "我是飛行能力" . PHP_EOL;
    }
}

class FirePower implements Power
{
    public function fight()
    {
        echo "我是開火能力" . PHP_EOL;
    }
}

class XrayPower implements Power
{
    public function fight()
    {
        echo "我是x光線能力" . PHP_EOL;
    }
}



class Superman
{
    private $power;

    public function __construct(Power $power)
    {
        $this->power = $power;
    }

    public function go()
    {
        $this->power->fight();
    }
}

$power1 = new FlyPower();
$power2 = new FirePower();
$power3 = new XrayPower();
$superman1 = new Superman($power1);
$superman1->go();
$superman2 = new Superman($power2);
$superman2->go();
$superman3 = new Superman($power3);
$superman3->go();

在這個例子中,SuperMan的構造方法需要一個參數,該參數必須實現Power接口,而這個參數是我們在類的外部進行實例化後傳入的,只要是Power的實現就都有效,所以相當於就把power注入到SuperMan中,這就是依賴注入。

這裏我們只是簡單了描述了依賴注入的原理,下面我們用代碼簡單實現一個我們自己的容器:

class Container
{
    protected $binds;

    protected $instances;

    public function bind($abstract, $concrete)
    {
        if ($concrete instanceof Closure) {
            $this->binds[$abstract] = $concrete;
        } else {
            $this->instances[$abstract] = $concrete;
        }
    }

    public function make($abstract, $parameters = [])
    {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        array_unshift($parameters, $this);

        return call_user_func_array($this->binds[$abstract], $parameters);
    }
}


// 創建容器
$container = new Container();
// bind方法:第一個參數可以理解爲要綁定的key,別名,第二個參數可以是一個回調函數,也可以是一個實例對象
$container->bind('flypower', function ($container) {
    return new FlyPower();
});
// bind的第二個參數爲回調函數的情況
$container->bind('firepower', new FirePower());
// bind的第二個參數爲實例對象的情況
$container->bind('superman', function ($container, $power) {
    return new Superman($container->make($power));
});

$superman1 = $container->make('superman', ['flypower']);
$superman2 = $container->make('superman', ['firepower']);
$superman1->go();// 輸出:我是飛行能力
$superman2->go();// 輸出:我是開火能力

上述例子是我們通過自己的理解實現的一個非常簡陋的容器,在laravel中,依賴注入是通過Ioc容器來實現的,而且自動化,更高效,省去了我們手動方式去注入依賴,在Ioc容器中全部自動幫你搞定。我們先來模仿laravel來實現自己的容器,代碼如下:

class Container
{
    protected $bindings = [];

    public function bind($abstract, $concrete = null, $shared = false)
    {
        if (!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }
        $this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];
    }


    protected function getClosure($abstract, $concrete)
    {
        return function ($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? "build" : "make";
            return $c->$method($concrete);
        };
    }

    public function make($abstract)
    {
        //這裏的註釋在調試時,建議打開,可以直觀的跟蹤程序的執行順序和流程
        //static $i = 1;
        //echo "make-".$i++.PHP_EOL;

        $concrete = $this->getConcrete($abstract);

        if ($this->isBuildable($concrete, $abstract)) {

            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
        return $object;
    }

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }


    protected function getConcrete($abstract)
    {
        if (!isset($this->bindings[$abstract])) {
            return $abstract;
        }
        return $this->bindings[$abstract]['concrete'];
    }


    public function build($concrete)
    {
        //這裏的註釋在調試時,建議打開,可以直觀的跟蹤程序的執行順序和流程
        //static $i = 1;
        //echo "build-".$i++.PHP_EOL;

        if ($concrete instanceof Closure) {
            return $concrete($this);
        }

        $ref = new ReflectionClass($concrete);

        if (!$ref->isInstantiable()) {
            echo "The $concrete is not instantiable.";
            exit;
        }
        $constructor = $ref->getConstructor();
        if (is_null($constructor)) {
            return new $concrete;
        }
        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);
        return $ref->newInstanceArgs($instances);
    }


    protected function getDependencies($parameters)
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if (is_null($dependency)) {
                $dependencies[] = null;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return (array)$dependencies;

    }

    protected function resolveClass(ReflectionParameter $parameter)
    {
        return $this->make($parameter->getClass()->name);
    }
}

$c = new Container();
$c->bind("Power", "FlyPower");
$c->bind("superman1", "Superman");
$superman1 = $c->make("superman1");
//$c->bind("Superman","Superman");
//$superman1 = $c->make("Superman");
$superman1->go(); //輸出:我是飛行能力

調試本代碼的時候,建議讀者朋友們打開在build()方法和make()方法中的註釋部分,可以更加直觀的看到make()和build()是如何被調用的,什麼時候被調用,一共調用了多少次,竊以爲laravel作者在這裏的調用有些繁瑣,本可以更快捷方便的實現,稍後我會把自己認爲比較完美的代碼貼出來。

yii中的容器

看完laravel的容器,相信讀者們已經都或多或少對容器有了一定的認知,其實目前市面上的php框架都是融入了容器的設計理念的,可以這樣說,一個現代化的框架一定是離不開容器的,那麼我們不妨來看看php界另一個重量級框架——yii,是如何實現自己的容器的。

在yii的容器中,Container類維護了5個數組,這是功能實現的基礎:

private $_singletons = []; //用於保存單例 Singleton 對象,以對象類型爲鍵
private $_definitions = []; //用於保存依賴的定義,以對象類型爲鍵
private $_params = []; //用於保存構造函數的參數,以對象類型爲鍵
private $_reflections = []; //用於緩存 ReflectionClass 對象,以類名或接口名爲鍵
private $_dependencies = []; //用於緩存依賴信息,以類名或接口名爲鍵

在yii的container中,是通過Container->set()方法來註冊依賴的,用法簡單做一下說明:

// 註冊一個接口,當一個類依賴於該接口時,定義中的類會自動被實例化,並供有依賴需要的類使用
// $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 註冊一個別名,當調用 $container->get('foo') 時,可以得到一個yii\db\Connection實例
// $_definition['foo', 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');

// 用一個配置數組來註冊一個類,需要這個類的實例時,這個配置數組會發生作用
// $_definition['yii\db\Connection'] = [...]
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一個配置數組來註冊一個別名,由於別名的類型不詳,因此配置數組中需要有class元素
// $_definition['db'] = [...]
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一個 PHP callable 來註冊一個別名,每次引用這個別名時,這個 callable 都會被調用
// $_definition['db'] = function(...){...}
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// 用一個對象來註冊一個別名,每次引用這個別名時,這個對象都會被引用
// $_definition['pageCache'] = anInstanceOfFileCache
$container->set('pageCache', new FileCache);

容器中對象的獲取是通過get()方法:

  • Container->get()用於返回一個對象或一個別名所代表的對象,可以是已經註冊好依賴的,也可以是沒有註冊過依賴的。無論是哪種情況,Yii 均會自動解析將要獲取的對象對外部的依賴。

  • get()解析依賴獲取對象是一個自動遞歸的過程,也就是說,當要獲取的對象依賴於其他對象時,Yii會自動獲取這些對象及其所依賴的下層對象的實例。同時,即使對於未定義的依賴,DI 容器通過PHP 的Reflection機制,也可以自動解析出當前對象的依賴來。

  • get()不直接實例化對象,也不直接解析依賴信息,而是通過build()來實例化對象和解析依賴,這點跟laravel容器的實現機制其實是一模一樣的。

我們仍然用最上面Superman的例子,看一下在yii的容器裏是如何操作的(該例子只是爲了方便理解,沒有考慮命名空間的情況):

$container = new Container();               //創建容器
$container->set("Power", "FlyPower");       //在需要使用接口Power時,用FlyPower類來作爲實現
$container->set("superman2", "Superman");   //爲Superman類定義一個別名
$superman2 = $container->get("superman2");  //獲取容器裏別名對應的對象

看到了嗎,是不是和laravel的容器如出一撤,laravel中容器的bind()方法對應yii中容器的set()方法,laravel中容器的make()方法對應yii中容器的get()方法。

需要特別關注的一點是,yii引入了服務定位器(Service Locator)作爲容器的昇華(自己的理解),你可千萬不要被這個名字嚇到了,服務定位器並不是什麼高大上的玩意,它背後的實現其實都離不開容器,我對yii的服務定位器作一個我自己認爲比較簡單的描述:

在網站程序啓動時,把所有程序運行必要的組件,配置都放入服務定位器中,注意這裏只是放入(其實就是調用了ServiceLocator::set()方法),當程序使用到這個組件的時候,再去取出來(調用ServiceLocator::get()方法),這個取的過程需要容器container的配合,因爲要解析依賴,該實例化的要實例化,就是這麼簡單。

Yii容器和Service Locator的工作流程大致如下:

  1. Yii類提供了一個靜態的$container成員變量用於引用 DI 容器。在入口腳本中,會創建一個 DI 容器,並賦值給這個 $container。

  2. Service Locator通過Yii::createObject()來獲取實例,而這個Yii::createObject()是調用了DI容器的yii\di\Container::get()來向Yii::$container索要實例的。因此,Service Locator最終是通過DI容器來創建和獲取實例的。

關於服務定位器的示例代碼:

//創建Service Locator
$serviceLocator = new yii\di\ServiceLocator();

//註冊一個服務
$serviceLocator->set('superman3', [
    'class' => 'Superman',
    //'config' => [],//如果需要其他配置,這個數組就用得上
]);

//服務定位器裏服務的訪問方式有兩種:
//1.使用訪問屬性的方法訪問這個服務
$serviceLocator->superman3->go();
//2.使用服務定位器的get()方法訪問這個服務
$serviceLocator->get('superman3')->go();

最後,做一個總結

因爲這篇文章是專門講容器的,所以並不會從其他方面分析框架的優劣,只想從容器實現的角度來說一下我對laravel和yii的見解。雖然laravel和yii在容器實現上如出一轍,但是竊以爲yii的實現表現得更有條理,也更好理解,而且在註冊依賴的時候,可以支持數組,回調函數,類名等,顯得更爲強大,服務定位器的引入,從操作和表現來看,更爲直接和語義化。當然這只是我的理解,因爲我對laravel其實也不是非常熟悉,說的不對的地方,大家輕噴。

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