目前我正在尝试学习Zend Framework,因此我购买了“Zend Framework in Action”一书。
在第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中还有其他一些方法吗?
答案 0 :(得分:10)
首先,值得一提的是,控制器应该只需要功能测试,尽管所有逻辑都属于模型。
以下是我的Action Controller实现的摘录,它解决了以下问题:
$_GET
中传递数组我的完整代码还允许生成基于或要求或处理动作参数的规范URL(用于SEO或统计数据的唯一页面哈希)。为此,我使用了这个抽象的Action Controller和自定义的Request对象,但这不是我们在这里讨论的情况。
显然,我使用Reflections来自动确定动作参数和依赖对象。
这是一个巨大的优势并简化了代码,但也会对性能产生影响(对于我的应用程序和服务器而言,这一点很小并且不重要),但是您可以实现一些缓存以加快速度。计算好处和缺点,然后决定。
DocBlock注释正在成为一个众所周知的行业标准,并且为了评估目的而对其进行解析变得更加流行(例如,Doctrine 2)。我在许多应用程序中使用了这种技术,并且效果很好。
写这门课我受到Actions, now with params!和Jani Hartikainen's blog post的启发。
所以,这是代码:
<?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 {}
并像往常一样为行动提供注释(至少你已经应该这样做了。)。
e.g。
/**
* @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会根据操作参数自动生成)
答案 1 :(得分:5)
好的,我就这样做了:
作为IoC Framework,我使用了this component of the symfony framework(但我没有下载最新版本,我使用过之前在项目中使用过的旧版本...记住这一点!)。我在/library/ioc/lib/
下添加了它的类。
我在Bootstrap.php
中添加了这些init函数,以便注册IoC框架的自动加载器:
protected function _initIocFrameworkAutoloader()
{
require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');
sfServiceContainerAutoloader::register();
}
接下来,我在application.ini
中进行了一些设置,设置了布线xml的路径,并允许禁用自动依赖注入e。 G。在单元测试中:
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似乎没有提供一个钩子,我可以用自定义控制器工厂创建控制器实例,所以这是我想出的最好的......
答案 2 :(得分:3)
我目前正在研究相同的问题,经过深入研究后,我决定使用Symfony Dependency Injection组件。您可以从官方网站http://symfony.com/doc/current/book/service_container.html获得好消息。
我在bootstrap中构建了自定义 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组件,而不是第一个版本。
答案 3 :(得分:1)
您也可以使用PHP-DI ZF桥:http://php-di.org/doc/frameworks/zf1.html
我知道这个问题真的很老但是在ZF1中寻找DI时搜索引擎会出现相当高的问题所以我想我会添加一个并不需要你自己编写所有内容的解决方案
答案 4 :(得分:0)
使用Zend Framework 3的服务管理器。
官方文件:
https://zendframework.github.io/zend-servicemanager/
示例:
控制器 `
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
我希望它有助于某人