菜菜鸟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
在当前的案例中有一个注意的功能。每一个配置环境只在当前有效:意思是说你须要在每一个执行环境中产生一个容器。咱们的建议是,你按照这样作,在你的环境中使用指定的容器类。



未完待续,谢谢......

相关文章
相关标签/搜索