目前我正在尝试学习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中还有其他一些方法吗?
首先,值得一提的是,控制器应该只需要功能测试,尽管所有逻辑都属于模型.
以下是我的Action Controller实现的摘录,它解决了以下问题:
允许将任何依赖项注入操作
验证动作参数,例如,$_GET
当期望整数时,您可能不会传入数组
我的完整代码还允许生成基于或要求或处理动作参数的规范URL(用于SEO或统计数据的唯一页面哈希).为此,我使用这个抽象的Action Controller和自定义Request对象,但这不是我们在这里讨论的情况.
显然,我使用Reflections来自动确定动作参数和依赖对象.
这是一个巨大的优势并简化了代码,但也对性能产生了影响(在我的应用程序和服务器的情况下,这一点很小并且不重要),但是您可以实现一些缓存来加速它.计算好处和缺点,然后决定.
DocBlock注释正在成为一个众所周知的行业标准,并且为了评估目的而对其进行解析变得更加流行(例如,Doctrine 2).我在许多应用程序中使用了这种技术,它运行良好.
写这堂课我受到了动作的启发,现在有了参数!和Jani Hartikainen的博客文章.
所以,这是代码:
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是必需的,但是我使用Netbeans IDE,因此DocBlock是根据动作参数自动生成的)
好的,我就是这样做的:
作为IoC框架,我使用了symfony框架的这个组件(但是我没有下载最新版本,我使用了之前在项目中使用的旧版本...记住这一点!).我在下面添加了它的类/library/ioc/lib/
.
我在我的程序Bootstrap.php
中添加了这些init函数,以便注册IoC框架的自动加载器:
protected function _initIocFrameworkAutoloader() { require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php'); sfServiceContainerAutoloader::register(); }
接下来,我做了一些设置,application.ini
其中设置了布线xml的路径,并允许禁用自动依赖注入,例如在单元测试中:
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似乎没有提供一个钩子,我可以用自定义控制器工厂创建控制器实例,所以这是我想出的最好的...