ActionInjections
Master:
, (*1)
Introduction
ZF2 Module wich allows to inject dependencies in controller action for better incapsulate testing and remove controller dependency from ServiceLocator., (*2)
Problem:
I have simply CRUD Controller, with actions "create", "update", "show", "delete", i have 3 dependency in action "delete": ViewModel, Request, some Service., (*3)
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
public function deleteTimesheetAction() {
$view = new ViewModel();
if (!$this->getRequest()->isPost()) {
return $view;
}
$timesheetDeleteService = $this->getServiceLocator()->get('Timesheet\Timesheet\Service\Delete');
$timesheetId = $this->getRequest()->getPost()->get('id', 0);
if (!$timesheetDeleteService->delete($timesheetId)) {
$view->errors = $timesheetDeleteService->getErrors();
}
return $view;
}
//...
}
in this case i can't easy test this controller, because:
1. I can't mock ViewModel (constructor calling)
2. Very difficult create test for this $this->getRequest()->getPost()->get('id', 0);
3. Nobody understand dependencies in current controller, because $this->getServiceLocator()->get('SomeService') inside controller - is bad practice, (*4)
Ok, refactor it.., (*5)
Problem:
I have simply CRUD Controller, with actions "create", "update", "show", "delete", i have 3 dependency in action "delete": ViewModel, Request, some Service. For height testability i add all dependencies in Controller::__constructor()., (*6)
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
/**
* @var BaseFinder
*/
private $timesheetFinder;
/**
* @var BaseFinder
*/
private $calendarFinder;
/**
* @var CreateInterface
*/
private $createService;
/**
* @var UpdateInterface
*/
private $updateService;
/**
* @var DeleteInterface
*/
private $deleteService;
/**
* @var AjaxViewModel
*/
private $view;
public function __construct(
BaseFinder $timesheetFinder,
BaseFinder $calendarFinder,
CreateInterface $timesheetCreateService,
UpdateInterface $timesheetUpdateService,
DeleteInterface $timesheetDeleteService,
AjaxViewModel $view)
{
$this->timesheetFinder = $timesheetFinder;
$this->calendarFinder = $calendarFinder;
$this->createService = $timesheetCreateService;
$this->updateService = $timesheetUpdateService;
$this->deleteService = $timesheetDeleteService;
$this->view = $view;
}
public function deleteTimesheetAction() {
$view = new ViewModel();
if (!$this->getRequest()->isPost()) {
return $this->view;
}
$timesheetId = $this->getRequest()->getPost()->get('id', 0);
if (!$this->deleteService->delete($timesheetId)) {
$this->view->errors = $this->deleteService->getErrors();
}
return $this->view;
}
// ...
}
class AjaxControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$serviceManager = $serviceLocator->getServiceLocator();
return new AjaxController(
$serviceManager->get('Timesheet\Timesheet\Service\Finder'),
$serviceManager->get('Calendar\Calendar\Service\Finder'),
$serviceManager->get('Timesheet\Timesheet\Service\Create'),
$serviceManager->get('Timesheet\Timesheet\Service\Update'),
$serviceManager->get('Timesheet\Timesheet\Service\Delete'),
$serviceManager->get('Timesheet\Controller\ViewModel\AjaxViewModel')
);
}
}
in this case i can easy test this controller, but:
1. I have to big __constructor (almost god object)
2. How many mock's i must create for test on method "delete"?
3. Nobody understand where i use each dependency in current controller
4. I must test ControllerFactory, (*7)
Solution: use t4web/ActionInjections
Add in your module.config.php section controller_action_injections
, (*8)
'controller_action_injections' => array(
'Timesheet\Controller\User\AjaxController' => array(
'deleteTimesheetAction' => array(
'request',
'Timesheet\Controller\ViewModel\AjaxViewModel',
'Timesheet\Timesheet\Service\Delete',
),
),
),
where request
, Timesheet\Controller\ViewModel\AjaxViewModel
, Timesheet\Timesheet\Service\Delete
your dependecies, and just use it in your controller action, (*9)
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
public function deleteTimesheetAction(HttpRequest $request, AjaxViewModel $view, DeleteInterface $timesheetDeleteService) {
if (!$request->isPost()) {
return $view;
}
$timesheetId = $request->getPost()->get('id', 0);
if (!$timesheetDeleteService->delete($timesheetId)) {
$view->setErrors($timesheetDeleteService->getErrors());
}
return $view;
}
//...
}
and test it:, (*10)
class AjaxControllerTest extends \PHPUnit_Framework_TestCase {
public function testDeleteTimesheetAction_Delete_ReturnView() {
$requestMock = $this->getMockBuilder('Zend\Http\PhpEnvironment\Request')->disableOriginalConstructor()->getMock();
$timesheetDeleteServiceMock = $this->getMockBuilder('T4webBase\Domain\Service\Delete')->disableOriginalConstructor()->getMock();
$ajaxViewModel = new AjaxViewModel();
$timesheetId = 1;
$parameters = new Parameters(array('id' => $timesheetId));
$requestMock->expects($this->once())
->method('isPost')
->will($this->returnValue(true));
$requestMock->expects($this->once())
->method('getPost')
->will($this->returnValue($parameters));
$timesheetDeleteServiceMock->expects($this->once())
->method('delete')
->with($this->equalTo($timesheetId))
->will($this->returnValue(true));
$controller = new AjaxController();
/** @var $result AjaxViewModel */
$result = $controller->deleteTimesheetAction($requestMock, $ajaxViewModel, $timesheetDeleteServiceMock);
$this->assertEquals($ajaxViewModel, $result);
}
//...
}
very fast, easy, readable, incapsulate unit test., (*11)
Requirements
Installation
Main Setup
By cloning project
Clone this project into your ./vendor/
directory., (*12)
With composer
Add this project in your composer.json:, (*13)
"repositories": [
{
"type": "git",
"url": "https://github.com/t4web/actioninjections.git"
}
],
"require": {
"t4web/actioninjections": "dev-master"
}
Now tell composer to download Authentication by running the command:, (*14)
$ php composer.phar update
Post installation
Not need enabling it in your application.config.php
file, just extends from T4webActionInjections\Mvc\Controller\AbstractActionController
, (*15)
Testing
Unit test runnig from authentication module directory., (*16)
$ phpunit