菜菜鳥Zend Framework 2 不徹底學習塗鴉(十三)-- 學習依賴注入

學習依賴注入

1、很是簡短的介紹 Di(Dependency Injection)

依賴注入(Dependency Injection)是一個已經在 web 界討論不少次的概念。這裏的快速入門以此爲目的,咱們經過如下簡單的代碼解釋依賴注入的行爲: php

$b = new B(new A());

以上的代碼中,A 是 B的依賴(個人理解:也就是說 B 依賴於 A,若是沒有 A 那麼 B 也沒法正常工做),A 注入到 B 裏面。若是你不熟悉依賴注入的概念,這裏有幾篇很好的文章: Analogy(Matthew Weier O’Phinney), Learning DI(Ralph Schindler)和  Series on DI(Fabien Potencier)。 html


2、最簡單的使用案例(兩個類,一個消耗另外一個)

在這個最簡單的使用案例裏,開發者能夠有一個類(A)經過構造函數被另外一個類(B)消耗。經過構造函數來進行依賴注入,這須要一個 A 類型的對象在 B 類型的對象以前實例化,以便 A 能夠注入到 B 內部。 html5

namespace My {

    class A
    {
        /* 一些有用的功能代碼 */
    }

    class B
    {
        protected $a = null;
        public function __construct(A $a)
        {
            $this->a = $a;
        }
    }
}
要手動建立 B,開發者須要跟隨這個工做流程,或者與下面的工做流程相似的動做
$b = new B(new A());
個人理解:
從上面類的定義代碼能夠知道,要實例化 B 以前,必需要先實例化 A,而後將 A 的實例化對象做爲參數傳遞給 B 的構造函數。舉個例子,有兩個類,一個是登陸類(Login Class),另外一個是數據庫類(DB Class)。在實現登陸功能時,先要實例化一個數據庫類的對象,而後在實例化登陸類的時候把這個數據庫對象做爲參數傳遞給登陸類的構造函數,這樣當登陸類實例化之後才能在數據庫中進行用戶名和密碼的讀取。
若是這個工做流程在你的應用程序中至始至終重複不少次,這創造了一個使用 DRY D on't R epeat Y ourself)代碼的機會。有幾個方法來實現這個,使用依賴注入容器是幾個解決方案中的一個。Zend 的依賴注入容器 Zend\Di\Di,注意上面的使用案例沒有使用配置(假設你全部的自動加載都已經正確配置)如下使用
$di = new Zend\Di\Di;
$b = $di->get('My\B'); // 將產生一個消耗 A 對象的 B 對象
此外,使用 Di::get() 方法,你要確保在隨後的調用中返回徹底相同的對象。要強制每一個請求建立新的對象,要使用 Di::newInstance() 方法:
$b = $di->newInstance('My\B');
讓咱們假設,A在被建立以前請求一些配置。咱們擴充了以前的使用案例(咱們額外添加了第三個類)
namespace My {

    class A
    {
        protected $username = null;
        protected $password = null;
        public function __construct($username, $password)
        {
            $this->username = $username;
            $this->password = $password;
        }
    }

    class B
    {
        protected $a = null;
        public function __construct(A $a)
        {
            $this->a = $a;
        }
    }

    class C
    {
        protected $b = null;
        public function __construct(B $b)
        {
            $this->b = $b;
        }
    }

}
上面的代碼,咱們須要確保咱們的Di可以看到類A以及一些配置值(廣義的講就是通常的標量)。要實現這個目的,咱們須要和InstanceManager交互:
$di = new Zend\Di\Di;
$di->getInstanceManager()->setProperty('A', 'username', 'MyUsernameValue');
$di->getInstanceManager()->setProperty('A', 'password', 'MyHardToGuessPassword%$#');
如今咱們的容器中已經有指了,當建立A時可使用。咱們的新目標是要有一個C對象,這個C對象消耗B而且依次消耗A。同樣的使用場景:
$c = $di->get('My\C');
// or
$c = $di->newInstance('My\C');
足夠簡單了吧,可是若是咱們要在調用的時候傳遞參數該怎麼作呢?假設一個默認的Di對象( $di = new Zend\Di\Di()沒有對InstanceManager進行任何配置 )咱們能夠這樣作:
$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);

$c = $di->get('My\C', $parameters);
// or
$c = $di->newInstance('My\C', $parameters);
構造函數注入不是注入支持的惟一類型。其它很是受歡迎的注入方法一樣被支持:setter注入。setter注入容許的使用場景和咱們先前的例子差很少,除了B類,B類如今改爲以下代碼:
namespace My {
    class B
    {
        protected $a;
        public function setA(A $a)
        {
            $this->a = $a;
        }
    }
}
因爲這個方法以「set」做爲前綴而且後面跟着一個大寫字母,Di知道這個方法是用在setter注入的,再次使用 $c = $di->get('C'),Di知道在須要建立一個C類型的對象時如何填寫依賴關係。

建立一些其它方法來肯定類之間的連接,如:接口注入,基於註釋的注入 web

3、最簡單的沒有類型提示(Type-hints)的使用案例

若是你的代碼美圓後類型提示(Type-hints)或者使用第三方沒有類型提示的代碼,但須要實現依賴注入,依然可使用Di,但你須要明確的描述你的依賴關係。爲了實現這個,你須要與定義之一進行相互做用,它可以容許開發者與描述,對象和類之間的映射關係。這個特殊的定義叫BuilderDefinition,它能夠和RuntimeDefinition一塊兒工做或者替代RuntimeDefinition。 shell

定義是Di的一部分,它試圖描述類之間的關係,因此Di::newInstance() 和 Di::get() 能夠知道一個特別的類/對象須要填寫哪些依賴。沒有配置狀況下,Di將使用RuntimeDefinition,它在你的代碼中使用反射和類型標籤來肯定依賴關係。沒有類型標籤,它會假設全部的依賴關係是標量或者必須的配置參數。 數據庫

BuilderDefinition它能夠與RuntimeDefinition一同合做(技術上講,經過AggregateDefinition它能夠與任何定義一同合做),容許你經過編程來描述對象映射。讓咱們來一個例子,咱們上文說到的A/B/C的使用場景,咱們改變了B類的代碼,以下: 編程

namespace My {
    class B
    {
        protected $a;
        public function setA($a)
        {
            $this->a = $a;
        }
    }
}
注意到,惟一的改變是setA如今不包含任何類型標籤的信息

use Zend\Di\Di;
use Zend\Di\Definition;
use Zend\Di\Definition\Builder;

// Describe this class:
$builder = new Definition\BuilderDefinition;
$builder->addClass(($class = new Builder\PhpClass));

$class->setName('My\B');
$class->addInjectableMethod(($im = new Builder\InjectableMethod));

$im->setName('setA');
$im->addParameter('a', 'My\A');

// Use both our Builder Definition as well as the default
// RuntimeDefinition, builder first
$aDef = new Definition\AggregateDefinition;
$aDef->addDefinition($builder);
$aDef->addDefinition(new Definition\RuntimeDefinition);

// Now make sure the Di understands it
$di = new Di;
$di->setDefinition($aDef);

// and finally, create C
$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);

$c = $di->get('My\C', $parameters);

上述使用場景提供了通用的樣子,你能夠確保它與依賴注入容器一塊兒工做。在一個理想世界,你全部的代碼都有適當的類型提示和/或將使用映射策略,下降了大量的引導工做,須要作的就是爲了擁有完整的定義,它可以實例化你可能須要的對象。 數組


4、很是簡單的編譯定義的使用場景

沒有進入細節,正如你想到的,PHP的核心對Di並不友善。即開即用,Di使用RuntimeDefinition經過PHP的Reflection擴展來分辨全部的類映射。事實上PHP沒有真正的應用層級能都在請求之間將對象存儲在內容中的能力,有個和Java和.Net解決方案相似的方法,可是這個方法要比Java和.Net中的方法低效。(Java和.Net是應用層級將對象存儲在內存中的語言) app

爲了減小這個缺點,Zend\Di有幾個功能,可以圍繞依賴注入創建預編譯不少高開銷的任務。值得注意的是RuntimeDefinition是默認使用的,並且是惟一定義並按需查詢的。其他定義的對象都是被聚集和和存儲在磁盤上,這是一種高性能的方法。 ide

理想狀態下,第三方代碼將攜帶一個預編譯的定義來各類各樣關係和每一個類實例的參數/屬性。在第三方,這個定義將被構建成部署的一部分或者包。當不是這樣的狀況下,你能夠經過除了RuntimeDefinition以外提供的任何定義類型來建立這些定義。這裏是每一個定義類型分解工做:

  • AggregateDefinition - Aggregates多重定義各類各樣的類型。當查找一個類時,它按順序將定義提供給Aggregate
  • ArrayDefinition - 這個定義取出一個數組的信息而且經過Zend\Di\Definition提供的接口展現出來,適合Di或者一個AggregateDefinition的場景.
  • BuilderDefinition - 建立一個基於包含各類對象圖Builder\PhpClass對象和Builder\InjectionMethod對象描述映射須要的目標代碼庫的定義
  • Compiler - 這實際上不是一個定義,但它是ArrayDefinition產生過程當中基於的一個代碼掃描器(Zend\Code\Scanner\DirectoryScanner或者Zend\Code\Scanner\FileScanner

下面是一個經過DirectoryScanner產生定義的過程例子

$compiler = new Zend\Di\Definition\Compiler();
$compiler->addCodeScannerDirectory(
    new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')
);
$definition = $compiler->compile();
這個定義能夠直接的使用Di(假設以上A、B、C場景中每一個類保存爲磁盤上的一個文件)
$di = new Zend\Di\Di;
$di->setDefinition($definition);
$di->getInstanceManager()->setProperty('My\A', 'username', 'foo');
$di->getInstanceManager()->setProperty('My\A', 'password', 'bar');
$c = $di->get('My\C');
一種堅持編譯定義的策略以下
if (!file_exists(__DIR__ . '/di-definition.php') && $isProduction) {
    $compiler = new Zend\Di\Definition\Compiler();
    $compiler->addCodeScannerDirectory(
        new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')
    );
    $definition = $compiler->compile();
    file_put_contents(
        __DIR__ . '/di-definition.php',
        '<?php return ' . var_export($definition->toArray(), true) . ';'
    );
} else {
    $definition = new Zend\Di\Definition\ArrayDefinition(
        include __DIR__ . '/di-definition.php'
    );
}

// $definition can now be used; in a production system it will be written
// to disk.
由於 Zend\Code\Scanner不包含文件,內部包含的類沒有調用到內存中。相反 Zend\Code\Scanner使用標記化來肯定你文件的結構。這使它能適當的在開發和 在相同的請求內部使用,相同的請求時 隨着你任何一個應用程序的派遣的action。

5、建立一個預編譯定義供別人使用

若是你是第三方開發人員,產生一個定義文件來描述你的代碼是有意義的,他人能夠利用這個定義而沒必要經過RuntimeDefinition來Reflect它,或者經過Compiler來建立它。要這麼作,使用上面說到的技巧。而不是在磁盤上寫結果數組,直接使用Zend\Code\Generator方法將信息寫入一個定義

// First, compile the information
$compiler = new Zend\Di\Definition\CompilerDefinition();
$compiler->addDirectoryScanner(
    new Zend\Code\Scanner\DirectoryScanner(__DIR__ . '/My/')
);
$compiler->compile();
$definition = $compiler->toArrayDefinition();

// Now, create a Definition class for this information
$codeGenerator = new Zend\Code\Generator\FileGenerator();
$codeGenerator->setClass(($class = new Zend\Code\Generator\ClassGenerator()));
$class->setNamespaceName('My');
$class->setName('DiDefinition');
$class->setExtendedClass('\Zend\Di\Definition\ArrayDefinition');
$class->addMethod(
    '__construct',
    array(),
    \Zend\Code\Generator\MethodGenerator::FLAG_PUBLIC,
    'parent::__construct(' . var_export($definition->toArray(), true) . ');'
);
file_put_contents(__DIR__ . '/My/DiDefinition.php', $codeGenerator->generate());

6、使用來自多源的多定義

在全部的現實中,你使用來自於不一樣地方的代碼,一些ZF代碼,一些第三方的代碼,固然還有你本身的代碼來構成你的應用程序。這裏有一個來自於多個地方消耗定義的方法:

use Zend\Di\Di;
use Zend\Di\Definition;
use Zend\Di\Definition\Builder;

$di = new Di;
$diDefAggregate = new Definition\Aggregate();

// first add in provided Definitions, for example
$diDefAggregate->addDefinition(new ThirdParty\Dbal\DiDefinition());
$diDefAggregate->addDefinition(new Zend\Controller\DiDefinition());

// for code that does not have TypeHints
$builder = new Definition\BuilderDefinition();
$builder->addClass(($class = Builder\PhpClass));
$class->addInjectionMethod(
    ($injectMethod = new Builder\InjectionMethod())
);
$injectMethod->setName('injectImplementation');
$injectMethod->addParameter(
'implementation', 'Class\For\Specific\Implementation'
);

// now, your application code
$compiler = new Definition\Compiler()
$compiler->addCodeScannerDirectory(
    new Zend\Code\Scanner\DirectoryScanner(__DIR__ . '/App/')
);
$appDefinition = $compiler->compile();
$diDefAggregate->addDefinition($appDefinition);

// now, pass in properties
$im = $di->getInstanceManager();

// this could come from Zend\Config\Config::toArray
$propertiesFromConfig = array(
    'ThirdParty\Dbal\DbAdapter' => array(
        'username' => 'someUsername',
        'password' => 'somePassword'
    ),
    'Zend\Controller\Helper\ContentType' => array(
        'default' => 'xhtml5'
    ),
);
$im->setProperties($propertiesFromConfig);

7、生成服務定位器

在生產中,你但願儘量的運行的快。依賴注入容器是爲速度而設計的,還須要作一個公平點的工做,解決運行時的參數和依賴性。什麼是能夠加速和刪除的查找呢?

Zend\Di\ServiceLocator\Generator組件能夠作到。它須要一個Di配置實例並且爲你生成一個服務定位器類,這個類爲你管理實例以及提供硬編碼,延遲加載實例化的實例。

getCodeGenerator()方法返回一個Zend\CodeGenerator\Php\PhpFile的實例,而後你就能夠寫一個類文件與新的服務定位器。在Generator中的方法容許你指定命名空間和類生成的服務定位器

做爲一個例子,考慮如下幾點:

use Zend\Di\ServiceLocator\Generator;

// $di is a fully configured DI instance
$generator = new Generator($di);

$generator->setNamespace('Application')
          ->setContainerClass('Context');
$file = $generator->getCodeGenerator();
$file->setFilename(__DIR__ . '/../Application/Context.php');
$file->write();
以上的代碼將放在 ../Application/Context.php中而且包含 Application\Context類。文件可能看上去以下:

<?php

namespace Application;

use Zend\Di\ServiceLocator;

class Context extends ServiceLocator
{

    public function get($name, array $params = array())
    {
        switch ($name) {
            case 'composed':
            case 'My\ComposedClass':
                return $this->getMyComposedClass();

            case 'struct':
            case 'My\Struct':
                return $this->getMyStruct();

            default:
                return parent::get($name, $params);
        }
    }

    public function getComposedClass()
    {
        if (isset($this->services['My\ComposedClass'])) {
            return $this->services['My\ComposedClass'];
        }

        $object = new \My\ComposedClass();
        $this->services['My\ComposedClass'] = $object;
        return $object;
    }
    public function getMyStruct()
    {
        if (isset($this->services['My\Struct'])) {
            return $this->services['My\Struct'];
        }

        $object = new \My\Struct();
        $this->services['My\Struct'] = $object;
        return $object;
    }

    public function getComposed()
    {
        return $this->get('My\ComposedClass');
    }

    public function getStruct()
    {
        return $this->get('My\Struct');
    }
}
要使用這個類,你就像使用一個Di容器那樣簡單的使用。

$container = new Application\Context;

$struct = $container->get('struct'); // My\Struct instance
在當前的案例中有一個注意的功能。每一個配置環境只在當前有效:意思是說你須要在每一個執行環境中產生一個容器。咱們的建議是,你按照這樣作,在你的環境中使用指定的容器類。



未完待續,謝謝......

相關文章
相關標籤/搜索