Zend framework 2/Введение в Dependency Injection

Введение в Dependency Injection

править

Dependency Injection


Короткое введение в Di

править

Про инъекции зависимостей упоминается очень часто. Особенно в ВЕБ. Мы же начнем пояснения с этого кода:

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


«А» является зависимостью «В» и инъекцируется(внедряется) в «В». Если Вы еще не сталкивались с инъекциями зависимостей, то есть несколько хороших статей(скоро будут переведены на нашем сайте):Matthew Weier O’Phinney’s Analogy, Ralph Schindler’s Learning DI, Fabien Potencier’s Series on DI.

Пример простого применения

править

В самом простом случае, это когда один класс (А) внедряется в другой (В) через конструктор ( __construct() ). Для обеспечения инъекции зависимости через конструктор, необходимо инициализировать объект типа «А» до использования объекта типа «В». Тогда «А» может быть инъекцирован в «В».

namespace My {
    
    class A
    {
        /* Some useful functionality */
    }
 
    class B
    {
        protected $a = null;
        public function __construct(A $a)
        {
            $this->a = $a;
        }
    }
}


Для создания разработчиком «В» вручную, он должен сделать так:

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


Если же в процессе разработки такой процесс начинает повторяться, то возникает желание минимализировать код, избавиться от излишних повторений. Есть несколько способов для достижения этой цели. Одним из них является использование контейнера (container) инъекции зависимостей. С контейнером инъекций зависимостей Zend Framework 2 Zend\Di\Di, вышеприведенный пример может быть реализован таким образом (подразумевается, что все настройки сделаны) :

$di = new Zend\Di\Di;
$b = $di->get('My\B'); // will produce a B object that is consuming an A object


Так же, при использовании метода Di::get(), можно быть уверенным, что будет вызван точно такой объект при следующих вызовах. Если же при новых запросвх нужно создавать новый объект, то можно воспользоваться методом Di::newInstance() :

$b = $di->newInstance('My\B');


Предположим, что «А» перед вызовом должен быть сконфигурирован(настроен) неким образом. Расширим наш предыдущий пример, добавив настройки и еще один класс:

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 сможет увидеть класс «А» с несколькими сконфигурированными значениями (обычно скалярные величины). Для этого

Нужно обеспечить взаимодействие с InstanceManager:

$di = new Zend\Di\Di;
$di->getInstanceManager()->setProperty('My\A', 'username', 'MyUsernameValue');
$di->getInstanceManager()->setProperty('My\A', 'password', 'MyHardToGuessPassword%$#');


Теперь, когда наш контейнер получил начальные настройки, он может использовать их при создании «А». И при создании «С» мы инъекцируем в него «В», а в «В» автоматически инъекцируется «А» . Все это делается очень просто:

$c = $di->get('My\C');
// или
$c = $di->newInstance('My\C');

Достаточно просто и понятно. Но если нужно назначать параметры во время вызова? Объект Di ( $di = newZend\Di\Di() ) можно создавать, передавая конфигурацию в него таким образом:

$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);

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


Инъекцировать объект можно не только через конструктор. Другой, достаточно распрастраненый способ созда ния инъекции является: setter injection (Установщик инъекций). Создание зависимости с setter injection для предыдущего примера выглядит так:

namespace My {
    class B
    {
        protected $a;
        public function setA(A $a)
        {
            $this->a = $a;
        }
    }
}


Метод начинается с префикса «set», далее без пробелов с большой буквы идет имя инъекцируемого объекта. Теперь Di знает, что в «В» инъекцируется «А». И что б создать объект «С» нужно сделать так:

$c = $di->get(‘C’);


Так же есть и другие методы для определения, какие зависимости существуют между классами. Такие, например, как инъекции интерфейсов (interface injection) и инъекции на основе аннотаций (annotation based injection).

Простое использование зависимостей без контроля типов (type-hint)

править

Если в Вашем коде отсутствует контроль типов или используете третестепенный код без контроля типов, но используете инъекции зависимостей Di, возможно возникнет ситуация, при которой придется описывать зависимость в явном виде. Для этого Вам потребуется использовать определения (definitions), которые позволят описать объекты и карты зависимостей между классами. Такое определение называется BuilderDefinition и может работать как с, так и вместо стандартного RuntimeDefinition.

Определения (definitions) являются частью Di, который позволяют описывать связи(отношения) между классами так, что, например, Di :: newInstance () и Di :: get () будут знать в каких именно зависимостях нуждаются создаваемые/вызываемые классы или объекты. При вызове Di без конфигураций будет использоваться RuntimeDefinition, который для карты зависимостей будет использовать отражения и контроль типов в коде. Если же контроль типов будет отсутствовать, он будет считать, что все зависимости скалярны, либо должны быть сконфигурированы каким-то образом.

BuilderDefinition может использоваться вместе с RuntimeDefinition (хотя технически он может быть использован с любыми зависимостями определенными как AggregateDefinition), позволяя описывать связи объектов. Давайте вернемся к нашему примеру с тремя классами А,В и С. Перепишем класс «В» таким образом:

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\BuilderDefinition;
   
// Describe this class:
$builder = new DefinitionBuilderDefinition;
$builder->addClass(($class = new Builder\PhpClass));
 
$class->setName('My\B');
$class->addInjectableMethod(($im = new Builder\InjectionMethod));
 
$im->setName('setA');
$im->addParameter('a', 'My\A');
 
// Use both our Builder Definition as well as the default
// RuntimeDefinition, builder first
$aDef = new Definition\ArrayDefinition;
$aDef->addDefinition($builder);
$aDef->addDefinition(new Definition\RuntimeDefinition);
 
// Now make sure the DependencyInjector understands it
$di = new Di;
$di->setDefinition($aDef);
 
// and finally, create C
$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);
 
$c = $di->get('My\C', $parameters);

Простое использование CompiledDefinition

править

Не вдаваясь в подробности – PHP не очень дружествен к инъекциям зависимостей. Если использовать Di «из коробки», то будет автоматически использоваться RuntimeDefinition, который будет брать все карты зависимостей классов из расширения PHP Reflection. Плюс PHP не умеет сохранять объекты приложений в памяти между запросами. В итоге получается, что аналоги на Java и .Net более производительны, так как там хранение объектов во внутренней памяти обеспеченно на уровне ядра.

Для устранения этих недостатков в Zend\Di имеется специальный функционал по предварительной компиляции наиболее ресурсоемких задач для связанных с инъекциями зависимостей. Так же стоит отметить, что только RuntimeDefinition, используемый по-умолчанию вызывается при каждом запросе. Остальные определения (definitions) специально скомпонованы и хранятся на диске с возможностью быстрого их извлечения при необходиости.

В иделае, третьестепенный код должен быть предварительно скомпилированным определением, которое описывает зависимости и настройки для классов, которые должны быть созданы. Также, это определение должно быть построено так, что бы быть вспомогательной частью для развертывания/упаковки этого третьестепенного кода. Если же так сделать не получается, то можно создать определение любого другого типа, кроме RuntimeDefinition. Типы определений(definition type):

1) AggregateDefinition – объединяет множество определений различных типов. При поиске класса ищет правила, назначенные для этого объединения.

2) ArrayDefinition – это определение принимает массив с нужной информацией и предоставляет доступ к нему через интерфейс Zend\Di\Defination, своместимый с Di и AggregateDefinition.

3) BuilderDefinition – определение, основанное на графах объектов, состоящих из различных объектов типа Zend\PhpClass и Builder\ InjectionMethod, которые описывают карты зависимостей классов/объектов.

4) Compiler – не является как таковым определением, но способно создавать ArrayDefinition основываясь на сканировании кода(Zend\Code\Scanner\DirectoryScanner , Zend\Code\Scanner\FileScanner)

Пример кода создания определения с помощью DirectoryScanner:

$compiler = new Zend\Di\CompilerDefinition(); $compiler->addCodeScannerDirectory(

   new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')

); $definition = $compiler->compile();


Это определение теперь может напрямую использоваться с Di (предыдущий пример с тремя классами должен быть сохранен на диске):

$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__
       span style= . '/di-definition.php'
   );

}

// $definition can now be used; in a production system it will be written // to disk.


Zend\Code\Scanner не подключает файлы, поэтому классы содержащиеся в файлах не загружаются в память. Вместо этого Zend\Code\Scanner используте токенизацию (tokenization) для определения структуры файла (лексический разбор). Это может быть полезным в процессе разработки и использования в пределах одного запроса как dispatched action.

Создание предварительно скомпилированных определений для использования

править

Если Вы разрабатывает третьестепенный код есть смысл создать файл-определение, описывающий ваш код. Тогда другим разработчикам, использующим Ваш код не придется делать отражение(Reflection) с помощью RuntimeDefintion или Compiler. Для этого используем ту же технику. Вместо записи конечного массива на диск, можно передать информацию напрямую в определение, используя Zend\Code\Generator:

// Сначала компилируем информацию $compiler = new Zend\Di\Definition\CompilerDefinition(); $compiler->addDirectoryScanner(

   new Zend\Code\ScannerDirectoryScanner(__DIR__ . '/My/')

); $compiler->compile(); $definition = $compiler->toArrayDefinition();

// затем создаем Definition class для этой информации $codeGenerator = new Zend\Code\Generator\FileGenerator(); $codeGenerator->setClass(($class = new Zend\Code\Generator\ClassGenerator())); $class->setNamespaceName('My'); $class->setName('DiDefinition'); $class->setExtendedClass('ZendDiDefinitionArrayDefinition'); $class->addMethod(

   '__construct',
   array(),
   \Zend\CodeGenerator\MethodGenerator::FLAG_PUBLIC,
   'parent::__construct(' . var_export($definition->toArray(), true) . ');'

); file_put_contents(__DIR__ . '/My/DiDefinition.php', $codeGenerator->generate());

Использование нескольких определений из различных источников

править

На практике Dі будете использовать код с нескольких источников: некоторый из Zend Framework 2, некоторый из дополнительных модулей, из основного приложения… Вот код, для создания определений с различных мест:

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

$di = new DependencyInjector; $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 ZendConfigConfig::toArray $propertiesFromConfig = array(

   'ThirdParty\Dbal\DbAdapter' => array(
       'username' => 'someUsername',
       'password' => 'somePassword'
   ),
   'Zend\Controller\Helper\ContentType' => array(
       'default' => 'xhtml5'
   ),

); $im->setProperties($propertiesFromConfig);

Создание ServiceLocators

править

В готовой версии(production) нужно, что б все работало максимально быстро и эффективно. Хотя Di и разработан для ускорения работы приложения, он все равно выполняет множество настроек и подключений зависимостей во время выполнения.

Для еще большего ускорения есть специальный компонент Zend\Di\ServiceLocator\Generator, который принимает уже сконфигурированный Di и создает service locator класс. Этот класс будет управлять экземплярами, а так же обеспечит «ленивую» загрузку и hardcoding.

Метод getCodeGenerator() возвращает экземпляр Zend\Code\Generator\Php\PhpFile, с помощью которого Вы сможете записать новый файл с классом Service Locator. С помощью методов генератора классов() можно указать пространство имен и класс для созданного Service Locator.

В качестве примера, рассмотрим следующий код:

use ZendDiServiceLocatorGenerator;

// $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 ZendDiServiceLocator;

class Context extends ServiceLocator {

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

           case 'struct':
           case 'MyStruct':
               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'); // MyStruct instance