Как использовать внедрение зависимостей в Zend Framework? - PullRequest
16 голосов
/ 05 сентября 2010

В настоящее время я пытаюсь изучить Zend Framework и поэтому купил книгу "Zend Framework в действии".

В главе 3 представлены базовая модель и контроллер, а также модульные тесты для них обоих.Базовый контроллер выглядит следующим образом:

class IndexController extends Zend_Controller_Action 
{
    public function indexAction()
    {
        $this->view->title = 'Welcome';
        $placesFinder = new Places();
        $this->view->places = $placesFinder->fetchLatest();
    }
}

Places - это класс модели, который выбирает последние места из базы данных.Что меня здесь беспокоит: как мне проверить IndexController в изоляции ?Поскольку ссылка на класс Places является «жестко закодированной», я не могу вставить любые заглушки или насмешки в IndexController.

Я хотел бы иметь что-то вроде этого:

class IndexController extends Zend_Controller_Action 
{
    private $placesFinder;

    // Here I can inject anything: mock, stub, the real instance
    public function setPlacesFinder($places)
    {
        $this->placesFinder = $places;
    }

    public function indexAction()
    {
        $this->view->title = 'Welcome';
        $this->view->places = $this->placesFinder->fetchLatest();
    }
}

Первый пример кода, который я разместил, определенно НЕ подходит для модульных тестов, поскольку IndexController нельзя тестировать изолированно.Второй намного лучше.Теперь мне просто нужен какой-то способ внедрить экземпляры модели в объекты контроллера.

Я знаю, что Zend Framework как таковой не имеет компонента для внедрения зависимостей.Но есть несколько хороших фреймворков для PHP, могут ли они использоваться вместе с Zend Framework?Или есть какой-то другой способ сделать это в Zend Framework?

Ответы [ 5 ]

10 голосов
/ 05 сентября 2010

Логика для моделей

Прежде всего, стоит отметить, что контроллеры должны нуждаться только в функциональных тестах, хотя вся логика принадлежит моделям.

Моя реализация

Вот выдержка из моей реализации Action Controller, которая решает следующие проблемы:

  • позволяет внедрить любую зависимость от действий
  • проверяет параметры действия, например, вы не можете передавать массив в $_GET, когда ожидается целое число

Мой полный код позволяет также генерировать канонический URL (для SEO или уникальный хэш страницы для статистики) на основе илиобязательные или обработанные параметры действий.Для этого я использую этот абстрактный Action Controller и пользовательский объект Request, но это не тот случай, который мы здесь обсуждаем.

Очевидно, я использую Reflections для автоматического определения параметров действия и объектов зависимостей.

Это огромное преимущество и упрощает код, но также влияет на производительность (минимально и не важно в случае моего приложения и сервера), но вы можете внедрить некоторое кэширование, чтобы ускорить его.Рассчитайте преимущества и недостатки, а затем решите.

Аннотации DocBlock становятся довольно известным отраслевым стандартом, и их разбор для целей оценки становится все более популярным (например, Doctrine 2).Я использовал эту технику для многих приложений, и она отлично работала.

При написании этого класса меня вдохновили Действия, теперь с параметрами! и В блоге Яни Хартикайнен .

Итак, вот код:

<?php

/**
 * Enchanced action controller
 *
 * Map request parameters to action method
 *
 * Important:
 * When you declare optional arguments with default parameters, 
 * they may not be perceded by optional arguments,
 * e.g.
 * @example
 * indexAction($username = 'tom', $pageid); // wrong
 * indexAction($pageid, $username = 'tom'); // OK
 * 
 * Each argument must have @param DocBlock
 * Order of @param DocBlocks *is* important
 * 
 * Allows to inject object dependency on actions:
 * @example
 *   * @param int $pageid
 *   * @param Default_Form_Test $form
 *   public function indexAction($pageid, Default_Form_Test $form = null)
 *
 */
abstract class Your_Controller_Action extends Zend_Controller_Action
{  
    /**
     *
     * @var array
     */
    protected $_basicTypes = array(
        'int', 'integer', 'bool', 'boolean',
        'string', 'array', 'object',
        'double', 'float'
    );

    /**
     * Detect whether dispatched action exists
     * 
     * @param string $action
     * @return bool 
     */
    protected function _hasAction($action)
    {
        if ($this->getInvokeArg('useCaseSensitiveActions')) {
            trigger_error(
                    'Using case sensitive actions without word separators' .
                    'is deprecated; please do not rely on this "feature"'
            );

            return true;
        }

        if (method_exists($this, $action)) {

            return true;
        }

        return false;
    }

    /**
     *
     * @param string $action
     * @return array of Zend_Reflection_Parameter objects
     */
    protected function _actionReflectionParams($action)
    {
        $reflMethod = new Zend_Reflection_Method($this, $action);
        $parameters = $reflMethod->getParameters();

        return $parameters;
    }

    /**
     *
     * @param Zend_Reflection_Parameter $parameter
     * @return string
     * @throws Your_Controller_Action_Exception when required @param is missing
     */
    protected function _getParameterType(Zend_Reflection_Parameter $parameter)
    {
        // get parameter type
        $reflClass = $parameter->getClass();

        if ($reflClass instanceof Zend_Reflection_Class) {
            $type = $reflClass->getName();
        } else if ($parameter->isArray()) {
            $type = 'array';
        } else {
            $type = $parameter->getType();
        }

        if (null === $type) {
            throw new Your_Controller_Action_Exception(
                    sprintf(
                            "Required @param DocBlock not found for '%s'", $parameter->getName()
                    )
            );
        }

        return $type;
    }

    /**
     *
     * @param Zend_Reflection_Parameter $parameter 
     * @return mixed
     * @throws Your_Controller_Action_Exception when required argument is missing
     */
    protected function _getParameterValue(Zend_Reflection_Parameter $parameter)
    {
        $name = $parameter->getName();
        $requestValue = $this->getRequest()->getParam($name);

        if (null !== $requestValue) {
            $value = $requestValue;
        } else if ($parameter->isDefaultValueAvailable()) {
            $value = $parameter->getDefaultValue();
        } else {
            if (!$parameter->isOptional()) {
                throw new Your_Controller_Action_Exception(
                        sprintf("Missing required value for argument: '%s'", $name));
            }

            $value = null;
        }

        return $value;
    }

    /**
     *
     * @param mixed $value 
     */
    protected function _fixValueType($value, $type)
    {
        if (in_array($type, $this->_basicTypes)) {
            settype($value, $type);
        }

        return $value;
    }

    /**
     * Dispatch the requested action
     *
     * @param   string $action Method name of action
     * @return  void
     */
    public function dispatch($action)
    {
        $request = $this->getRequest();

        // Notify helpers of action preDispatch state
        $this->_helper->notifyPreDispatch();

        $this->preDispatch();
        if ($request->isDispatched()) {
            // preDispatch() didn't change the action, so we can continue
            if ($this->_hasAction($action)) {

                $requestArgs = array();
                $dependencyObjects = array();
                $requiredArgs = array();

                foreach ($this->_actionReflectionParams($action) as $parameter) {
                    $type = $this->_getParameterType($parameter);
                    $name = $parameter->getName();
                    $value = $this->_getParameterValue($parameter);

                    if (!in_array($type, $this->_basicTypes)) {
                        if (!is_object($value)) {
                            $value = new $type($value);
                        }
                        $dependencyObjects[$name] = $value;
                    } else {
                        $value = $this->_fixValueType($value, $type);
                        $requestArgs[$name] = $value;
                    }

                    if (!$parameter->isOptional()) {
                        $requiredArgs[$name] = $value;
                    }
                }

                // handle canonical URLs here

                $allArgs = array_merge($requestArgs, $dependencyObjects);
                // dispatch the action with arguments
                call_user_func_array(array($this, $action), $allArgs);
            } else {
                $this->__call($action, array());
            }
            $this->postDispatch();
        }

        $this->_helper->notifyPostDispatch();
    }

}

Чтобы использовать это, просто:

Your_FineController extends Your_Controller_Action {}

и предоставьте аннотации к действиям, как обычно (по крайней мере, вы уже должны;).

например,

/**
 * @param int $id Mandatory parameter
 * @param string $sorting Not required parameter
 * @param Your_Model_Name $model Optional dependency object 
 */
public function indexAction($id, $sorting = null, Your_Model_Name $model = null) 
{
    // model has been already automatically instantiated if null
    $entry = $model->getOneById($id, $sorting);
}

(требуется DocBlock, однако я использую IDE Netbeans, поэтому DocBlock генерируется автоматически на основе аргументов действия)

5 голосов
/ 06 сентября 2010

Хорошо, вот как я это сделал:

В качестве IoC Framework я использовал этот компонент платформы Symfony (но я не загружал последнюю версию, я использовал более старую версию, которую я использовал в проектах раньше ... имейте это в виду!). Я добавил его классы под /library/ioc/lib/.

Я добавил эти функции инициализации в свой Bootstrap.php, чтобы зарегистрировать автозагрузчик инфраструктуры IoC:

protected function _initIocFrameworkAutoloader()
{
    require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');

    sfServiceContainerAutoloader::register();
}

Затем в application.ini были сделаны некоторые настройки, которые задают путь к xml проводки и позволяют отключить автоматическое внедрение зависимостей e. г. в модульных тестах:

ioc.controllers.wiringXml = APPLICATION_PATH "/objectconfiguration/controllers.xml"
ioc.controllers.enableIoc = 1

Затем я создал собственный класс компоновщика, который расширяет sfServiceContainerBuilder и поместил его в /library/MyStuff/Ioc/Builder.php. В этом тестовом проекте я держу все свои классы под /library/MyStuff/.

class MyStuff_Ioc_Builder extends sfServiceContainerBuilder
{
  public function initializeServiceInstance($service)
  {
      $serviceClass = get_class($service);
      $definition = $this->getServiceDefinition($serviceClass);


    foreach ($definition->getMethodCalls() as $call)
    {
      call_user_func_array(array($service, $call[0]), $this->resolveServices($this->resolveValue($call[1])));
    }

    if ($callable = $definition->getConfigurator())
    {
      if (is_array($callable) && is_object($callable[0]) && $callable[0] instanceof sfServiceReference)
      {
        $callable[0] = $this->getService((string) $callable[0]);
      }
      elseif (is_array($callable))
      {
        $callable[0] = $this->resolveValue($callable[0]);
      }

      if (!is_callable($callable))
      {
        throw new InvalidArgumentException(sprintf('The configure callable for class "%s" is not a callable.', get_class($service)));
      }

      call_user_func($callable, $service);
    }

  }
}

Наконец, я создал собственный класс контроллеров в /library/MyStuff/Controller.php, от которого наследуются все мои контроллеры:

class MyStuff_Controller extends Zend_Controller_Action {
    /**
     * @override
     */
    public function dispatch($action)
    {
        // NOTE: the application settings have to be saved 
        // in the registry with key "config"
        $config = Zend_Registry::get('config');

        if($config['ioc']['controllers']['enableIoc'])
        {
            $sc = new MyStuff_Ioc_Builder();

            $loader = new sfServiceContainerLoaderFileXml($sc);
            $loader->load($config['ioc']['controllers']['wiringXml']);

            $sc->initializeServiceInstance($this);
        }

        parent::dispatch($action);
    }
}
<ч />

В основном это использует IoC Framework для инициализации уже созданного экземпляра контроллера ($this). Простые тесты, которые я делал, казалось, делали то, что я хочу ... давайте посмотрим, как это работает в реальных жизненных ситуациях. ;)

Это все еще как-то непонятное исправление, но Zend Framework, похоже, не предоставляет возможности для создания экземпляра контроллера с фабрикой пользовательских контроллеров, так что это лучшее, что я придумал ...

3 голосов
/ 29 марта 2012

В настоящее время я работаю над тем же вопросом, и после глубокого исследования я решил использовать компонент Symfony Dependency Injection.Вы можете получить хорошую информацию с официального сайта http://symfony.com/doc/current/book/service_container.html.

Я создал собственный метод getContainer () в начальной загрузке, который теперь перезапускает сервисный контейнер, и его просто можно использовать в контроллерах, таких как

public function init()
{
    $sc = $this->getInvokeArg('bootstrap')->getContainer();
    $this->placesService = $sc->get('PlacesService');
}

Здесь вы можете найти, как это сделать http://blog.starreveld.com/2009/11/using-symfony-di-container-with.html. Но я изменил ContainerFactory из-за использования компонента Symfony2 вместо первой версии.

1 голос
/ 30 марта 2016

Вы можете также использовать мост PHP-DI ZF: http://php -di.org / doc / frameworks / zf1.html

Я знаю, что этот вопрос действительно старый, нопри поиске DI в ZF1 он появляется довольно высоко в поисковых системах, поэтому я подумал, что добавлю решение, которое не требует от вас написания всего этого самостоятельно.

0 голосов
/ 17 февраля 2017

С помощью Service Manager в Zend Framework 3.

Официальная документация:

https://zendframework.github.io/zend-servicemanager/

  1. Зависимости от вашего контроллера обычно вводятся инжектором DI Constructor.
  2. Я мог бы привести один пример, который вводит Фабрику, ответственную за создание экземпляра ViewModel, в контроллер.

Пример:

Контроллер `

class JsonController extends AbstractActionController
{
    private $_jsonFactory;
    private $_smsRepository;
    public function __construct(JsonFactory $jsonFactory, SmsRepository $smsRepository)
    {
        $this->_jsonFactory = $jsonFactory;
        $this->_smsRepository = $smsRepository;
    }
...
}

Creates the Controller

class JsonControllerFactory implements FactoryInterface
{
    /**
     * @param ContainerInterface $serviceManager
     * @param string $requestedName
     * @param array|null $options
     * @return JsonController
     */
    public function __invoke(ContainerInterface $serviceManager, $requestedName, array $options = null)
    {
        //improve using get method and callable
        $jsonModelFactory = new JsonFactory();
        $smsRepositoryClass = $serviceManager->get(SmsRepository::class);
        return new JsonController($jsonModelFactory, $smsRepositoryClass);
    }
}

` Полный пример на https://github.com/fmacias/SMSDispatcher

Надеюсь, это кому-нибудь поможет

...