Статьи

Внедрение зависимостей в PHP

Если вы работали с такими фреймворками, как Spring или Google Guice, вы поймете мощь и гибкость, которые обеспечивает внедрение зависимостей в вашу огромную базу кода. В последнее время я видел много проектов со множеством сложных компонентов, которые страдают от следующего синдрома:

Недостаток DI: 

<?php
class MyService {
   
   private $_dao;
   private $_reportingService;
   private $_emailService;
   private $_userService; 
   
   .... all the transitive dependent classes  
   private $_dbEngine;

   ...

   function __construct($dao = null, $reportingService = null, $emailService = null, $userService = null) {
       if(!$dao) {

          $this->_dbEngine = new DBEngine();
          $dao = new DAO($this->_dbEngine);
       }
       if(!$reportingService) {
          $reportingService = new ReportingService();
       }
       if(!$emailService) {
          $emailService = new EmailService();
       }

       ...... Continue initializing  ..... Ugh!

       $this->_dao = $dao;
       $this->_reportingService = $reportingService;
       
       ....... and on and on ...
   }
}
?>

Оставляя в стороне принципы, согласно которым классы должны делать что-то одно и быть очень сплоченными, часто вам придется писать классы, которые взаимодействуют со многими компонентами одновременно. Итак, вернемся к нашей проблеме …

Если приведенный выше код не достаточно плох, представьте себе, что DAO, ReportingService и / или EmailService каждый имеет свой длинный список аргументов конструктора . В этом случае нам нужно было знать о базовой технологии БД, используемой на уровне DAO. Теперь ваш класс обслуживания должен знать обо всех переходных зависимостях … Тьфу! ,

Чтобы облегчить создание экземпляра этого класса, мы используем потрясающую функцию PHP по умолчанию, которая позволяет каждому аргументу конструктора принимать значение NULL в качестве значения по умолчанию. Теперь, поскольку первый принимает значение NULL, все остальные должны указывать то же поведение. Итак, это позволяет вам создать экземпляр вашего сервиса как:

  $myService->  = new MyService();

Есть много людей, пишущих о лучших методах кодирования, которые придумали магические числа для того, сколько аргументов конструктора должно иметь класс. Некоторые люди говорят 3, другие 5 — я не особо думаю о числе, если ваши уроки читаются «хорошо».

Другие люди утверждают: вместо аргументов конструктора вы должны применять свои зависимости, используя функции установки. Конечно, это минимизирует код инициализации, но теперь ваши пользователи должны помнить об установке всех зависимостей при создании экземпляра вашего класса.

Итак, чтобы положить этому конец, я изучаю возможность создания (еще одной) инфраструктуры внедрения зависимостей, которая будет управлять всеми моими службами, компонентами (и их зависимостями) для PHP. Я исследовал множество фреймворков, размещенных в Github, и прочитал, как другие люди пытаются решить эту проблему. Я не был впечатлен ни одним из них.

Один быстрый недостаток, который я вижу, заключается в том, что природа PHP без состояния делает такие вещи, как контейнеры объектов, не очень практичными. С другой стороны, контейнеры объектов Java живут в памяти и, по сути, предоставляют вам реестр синглетов (также называемых бобами), которые вы можете загружать и использовать без явной инициализации. В PHP это становится проблемой, так как вы должны перестраивать контейнер объектов при каждом запросе. Для небольших графов зависимостей это, вероятно, не такая уж большая проблема, но для очень глубоких графов объектов вы можете начать видеть проблемы с производительностью.

Кроме того, я хотел бы сделать его таким же элегантным, как и Java-фреймворки, использующие такие преимущества, как аннотации и рефлексия. Это делает классы намного более читабельными и компактными. Для пользователя класс выше может выглядеть следующим образом:

Теперь с DI

<?php
class MyService {
   
   private $_dao;
   private $_reportingService;
   private $_emailService;
   private $_userService; 
   
   
   /**
    * Build a new service
    * @Inject(class=DAO)
    * @Inject(class=ReportingService)
    * @Inject(class=EmailService)
    * @Inject(class=UserService)
    */
   function __construct($dao, $reportingService, $emailService, $userService) {
       $this->_dao = $dao;
       $this->_reportingService = $reportingService;
       $this->_emailService = $emailService;
       ... 
   }
}
?>

Это выглядит намного чище и обратите внимание, что теперь моему классу не нужно знать о зависимостях DAO, ему не нужно знать о DBEngine. Они будут управляться контейнером объекта или контекстом приложения.

В идеале, эта структура внедрения зависимостей должна заимствовать часть словаря из аннотаций Spring и Java JSR-330 для DI. Вы можете использовать такие вещи, как:

  1. @Inject
  2. @Named
  3. @Qualifier
  4. @Сфера
  5. @Singleton
  6. поставщик

Особый интерес для меня представляют @Inject и Providers. С помощью провайдеров вы можете делегировать создание экземпляров определенного класса для вашей собственной пользовательской реализации. Многие вещи приходят на ум с этой идеей. Поскольку я перегружаю комментарии в PHP, я не знаю, как быстро я могу сканировать строки комментариев на наличие аннотаций. Я видел решения, в которых они полностью читают файл классов PHP и сканируют его вместо использования отражения PHP и таких вещей, как $ constructor-> getDocComment (). Однако меня больше всего беспокоит: будет ли это работать с приложениями, использующими кэширование кода операции, например APC? Я читал, что eAccelerator может удалять комментарии. Существует высокая вероятность того, что APC сделает то же самое. Если у вас есть какие-либо идеи, пожалуйста, оставьте комментарий, и мы можем обсудить это дальше.

С DI на месте создание экземпляра bean становится:

$myService = BeanFactory::forName('MyService');

При первом запросе этого класса он на лету строит свой граф зависимостей и помещает его и все его зависимости в контекст приложения для дальнейшего использования. Мы можем ускорить этот процесс и предоставить файл инициализации (YAML, XML или JSON) или карту PHP, которая отображает все компоненты с соответствующими именами. Это сделано для экономии времени при разборе текста. Многие решения там поддерживают это в некотором роде, так что я тоже буду его поддерживать.

Я все еще выкладываю все части, но я опубликую основную процедуру, чтобы найти некоторые отзывы:

public static function forName($classname, $depth = 1) {
  
 if(!class_exists($classname)) {
  // Either class does not exist (misspelled) or class
  // definition has not been included
  throw new \InvalidArgumentException('Class for name ['. $classname .'] does not exist. '.
  'Check class name and make sure class definition is included'); 
 }
  
 if(self::$BEAN_DEPTH == $depth) {
  // Don't even bother to do a look up, just instantiate without 
         // container
  return util\DIUtils::instantiate($classname);
 }
  
 $CONTEXT = ApplicationContext::get();
 $beanDef = null;
 $ns = '\\';
 $name = $classname;
 if(strpos($classname, '\\') !== false) {
  // check namespace defined with classname
  $classdef = split('\\', $classname);
  $ns = $classdef[0];
  $name = $classdef[1];
 }
 $beanDef = $CONTEXT->getBeanDefinition($name, $ns);
  
 if(!$beanDef) {
  $beanDef = BeanBuilder::build(
   array(
    'classname' => $name,
    'ns'        => $ns
    ));
  $CONTEXT->addBeanDefinition($beanDef);
 }
 return $beanDef->instance; 
 }

А функция BeanBuilder :: build выглядит следующим образом:

static function build(array $def) {
  
 $classname = $def ['classname'];
 $ns = $def ['ns'];
  
 $clazz = new \ReflectionClass ( $ns . $classname );
 if (!$clazz->isInstantiable ()) {
  throw new error\BeanInitializationException ( $classname );
 }
  
 // Instantiate class
 // In PHP 5.4 I could use, $newInstance = $clazz->newInstanceWithoutConstructor ();
 $annotations = array ();
 
 // Look for constructor annotations
 $constructor = $clazz->getConstructor();
 $constructorArgs = array();
 if($constructor) {
   
  // Provide simple instantiations of parameters by default
  // If constructor declaration has no type hinting, I cannot infer 
  // a default instantiation for that parameter, it should stay NULL in that case
  foreach($constructor->getParameters() as $param) {
   try {
    $c = $param->getClass();
    if($c && !$c->isInterface()) {
     $paramname = $c->getName();
     $constructorArgs[$paramname] = new $paramname;
    }
   }
   catch(\ReflectionException $re) {
    // ignore and leave parameter init as null
   }
  }
   
  // Collect dependent instances for constructor
  $beans = array(); 
  
  // Parse constructor annotation and look for any injection clauses
  foreach (parser\AnnotationParser::parse ($constructor->getDocComment()) as $a) {
   $a->addOption('method', model\InjectMethod::CONSTRUCTOR);
   $annotations [] = $a;
    
   // Recursively lookup/build dependencies 
   $beanname = $a->getOptionSingleValue("class");
    
   // Override simple instances with beans
   $constructorArgs[$beanname] = BeanFactory::forName($beanname);
  }
 }
 $newInstance = util\DIUtils::instantiate($clazz->getName(), $constructorArgs);
    
 $beanDef = new model\BeanDefinition ();
 $beanDef->ns = $clazz->getNamespaceName();
 $beanDef->classname = $classname;
 $beanDef->annotations = $annotations;
 $beanDef->instance = $newInstance;
 return $beanDef;
}

Пожалуйста, поделитесь своими мыслями и комментариями. Это возможно сделать в PHP, или есть лучший подход?