关于php:如何在Zend Framework中使用依赖注入?

How to use dependency injection in Zend Framework?

目前我正在努力学习Zend框架,因此我买了本书"Zend框架的实际应用"。

在第三章中,介绍了一个基本模型和控制器,以及它们的单元测试。基本控制器如下:

1
2
3
4
5
6
7
8
9
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中插入任何存根或模拟。

我想要的是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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框架本身没有用于依赖注入的组件。但是有一些很好的PHP框架,可以和Zend框架一起使用吗?或者在Zend框架中是否有其他方法可以做到这一点?


逻辑到模型

首先,值得一提的是,控制器应该只需要功能测试,尽管所有的逻辑都属于模型。

我的实施

下面是我的操作控制器实现的一个摘录,它解决了以下问题:

  • 允许对操作插入任何依赖项
  • 验证操作参数,例如,当需要整数时,不能在$_GET中传递数组

我的完整代码还允许生成基于标准URL(用于SEO或用于统计的唯一页哈希)或必需或处理的操作参数。为此,我使用这个抽象的动作控制器和自定义请求对象,但我们在这里讨论的情况并非如此。

显然,我使用反射自动确定动作参数和依赖对象。

这是一个巨大的优势,简化了代码,但对性能也有影响(对于我的应用程序和服务器来说,这是最小的,也不重要),但是您可以实现一些缓存来加快速度。计算出好处和缺点,然后决定。

docblock注释正在成为一个相当著名的行业标准,为了评估的目的而解析它变得更加流行(例如,条令2)。我在许多应用程序中使用了这种技术,它运行得很好。

写这节课的时候,我受到了动作的启发,现在和参数一起!以及Jani Hartikainen的博客帖子。

所以,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<?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();
    }

}

要使用它,只需:

1
Your_FineController extends Your_Controller_Action {}

并像往常一样为操作提供注释(至少您已经应该;)。

例如

1
2
3
4
5
6
7
8
9
10
/**
 * @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是必需的,但是我使用netbeans-ide,因此docblock是根据操作参数自动生成的)


好吧,我就是这样做的:

作为IOC框架,我使用了symfony框架的这个组件(但我没有下载最新版本,我使用了以前在项目中使用过的一个旧版本…。记住这一点!).I在/library/ioc/lib/下添加了它的类。

为了注册IOC框架的自动加载程序,我在我的Bootstrap.php中添加了这些init函数:

1
2
3
4
5
6
protected function _initIocFrameworkAutoloader()
{
    require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');

    sfServiceContainerAutoloader::register();
}

接下来,我在application.ini中进行了一些设置,这些设置设置了连接XML的路径,并允许禁用自动依赖项注入,例如在单元测试中:

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

然后,我创建了一个自定义生成器类,扩展了sfServiceContainerBuilder,并将其放在/library/MyStuff/Ioc/Builder.php下。在这个测试项目中,我把所有的类都放在/library/MyStuff/下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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中创建了一个自定义控制器类,所有控制器都继承自:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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框架来初始化已经创建的控制器实例($this)。我做的简单测试似乎满足了我的需求…让我们看看这在现实生活中的表现。;

它仍然是猴子补丁不知何故,但Zend框架似乎没有提供一个钩子,我可以创建一个自定义控制器工厂的控制器实例,所以这是我提出的最好的…


我目前正在研究同一个问题,经过深入研究,我决定使用symfony依赖注入组件。您可以从官方网站http://symfony.com/doc/current/book/service_container.html获得好的信息。

我已经在bootstrap中构建了自定义getContainer()方法,它现在重新生成服务容器,并且它可以简单地用于

1
2
3
4
5
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。但我改变了集装箱工厂,因为使用了symfony2组件,而不是第一个版本。


您也可以使用php-di zf桥:http://php-di.org/doc/frameworks/zf1.html

我知道这个问题真的很老,但是在ZF1中查找d i时,它在搜索引擎中出现得相当高,所以我想我会添加一个不需要你自己写的解决方案。


Zend Framework 3的服务管理器。

官方文件:

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

  • 控制器上的依赖项通常由DI构造函数注入器注入。
  • 我可以提供一个示例,将负责创建viewModel实例的工厂注入控制器。
  • 例子:

    控制器`

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class JsonController extends AbstractActionController
    {
        private $_jsonFactory;
        private $_smsRepository;
        public function __construct(JsonFactory $jsonFactory, SmsRepository $smsRepository)
        {
            $this->_jsonFactory = $jsonFactory;
            $this->_smsRepository = $smsRepository;
        }
    ...
    }


    Creates the Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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

    我希望它能帮助别人