I have this set of entities that we call nomenclators, which basically have an id field and a text-based field. The CRUD operations for these entities are virtually the same, just that in some of them the text field is called state while in others is area... and so on.
Given that, I created this base Controller
class NomenclatorsController extends Controller
{
use ValidatorTrait;
protected function deleteENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$spService = $this->get('spam_helper');
$resp = $spService->deleteEntitySpam("AplicacionBaseBundle:$entityName", $id);
if ($resp == false)
return new JsonResponse("error.$entityName.stillreferenced", Response::HTTP_FORBIDDEN);
return new JsonResponse('', Response::HTTP_ACCEPTED);
}
protected function listENTITYAction(Request $req, $entityName)
{
$size = $req->query->get('limit');
$page = $req->query->get('page');
$spService = $this->get('spam_helper');
$objectResp = $spService->allSpam("AplicacionBaseBundle:$entityName", $size, $page);
$arrayResp = $spService->spamsToArray($objectResp);
return new JsonResponse($arrayResp, Response::HTTP_ACCEPTED);
}
protected function updateENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$entity = null;
if (is_numeric($id)) {
$entity = $this->getDoctrine()->getRepository("AplicacionBaseBundle:$entityName")->find($id);
} else if (!is_numeric($id) || $id == null) {
//here comes the evil
eval('$entity=new \\AplicacionBaseBundle\\Entity\\' . $entityName . '();');
$entity->setEliminado(false);
$entity->setEmpresa($this->getUser()->getEmpresa());
}
$this->populateEntity($req->request, $entity);
$errors = $this->validate($entity);
if ($errors)
return new Response(json_encode($errors), Response::HTTP_BAD_REQUEST);
$spamService = $this->get('spam_helper');
$spamService->saveEntitySpam($entity);
}
//Override in children
protected function populateEntity($req, $entity)
{
}
}
So, each time I need to write a controller for one of these nomenclators I extend this NomenclatorsController and works like a charm.
The thing is in the updateENTITYAction I use eval for dynamic instantiation as you can see, but given all I have readed about how bad is eval I am confused now, and even when there is no user interaction in my case I want to know if there is a better way of doing this than eval and if there is any noticiable performance issue when using eval like this.
By the way I am working in a web json api with symfony and extend.js, which means no view is generated in the server,my controllers match a route and receive a sort of request params and do the work.
I've done something similar in the past. Since you are extending a base class using specific classes for each entity you can instance your entity from the controller that extends NomenclatorsController.
If one of your entities is called Foo you will have a FooController that extends NomenclatorsController. Just overwrite updateENTITYAction and pass back needed variables.
An example:
<?php
use AplicacionBaseBundle\Entity\Foo as Item;
class FooController extends NomenclatorsController
{
/**
* Displays a form to edit an existing item entity.
*
* #Route("/{id}/edit")
* #Method({"GET", "POST"})
* #Template()
* #param Request $request
* #param Item $item
* #return array|bool|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function updateENTITYAction(Request $request, Item $item)
{
return parent::updateENTITYAction($request, $item);
}
}
This way you are sending directly the entity to NomenclatorController and you don't even need to know the entityName.
Humm I'll me too advise you to avoid the eval function. It's slow and a bad practice.
What you want here is the factory pattern,
You could define a service to create the entites for you
#app/config/services.yml
app.factory.nomenclators:
class: YourNamespace\To\NomenclatorsFactory
And your factory might be like this
namespace YourNamespace\To;
use YourNamespace\To\Entity as Entites;
class NomenclatorsFactory {
// Populate this array with all your Nomenclators class names with constants OR with reflection if you have many
private $allowedNomemclators = [];
/**
* #param $entityName
* #return NomenclatorsInterface|false
*/
public function getEntity($entityName)
{
if(!is_string($entityName) || !in_array($entityName, $this->allowedNomemclators)) {
// Throw exception or exit false
return false;
}
return new $entityName;
}
}
Then you have to create the NomenclatorsInterface and define in it all the common methods between all your entities. Moreover define one more method getSomeGoodName, the job of this method is to return the good property (area or state)
With this structure your controller can only instances the Nomenclators entities and don't use anymore the eval evil method haha
Moreover you don't have to worry about about the state and area property
Ask if something isn't clear :D
I hope it help !
Related
I'm trying to implement a Results class that processes queries. So, simply put, you would have functions like this:
function all();
function first();
function paginate(int $perPage, int $pageNo = 1);
This works pretty well, the problem being that the IDE has no possible way of knowing the return type when this same results class is being used in multiple different query classes. Example:
UserQuery->results()->all() will return an array of User entities.
UserQuery->results()->first() will return a single User entity.
In some languages, you have generics, which means I could just use Results<User> in the UserQuery class and then my Results class could return T[] and T respectively.
One idea I had was to pass an empty entity as the constructor to the Results class and then try to use that property as the return type, but I couldn't figure this out. Is there any workaround to this? The main problem I'm trying to solve is IDE autocompletion and analysis, so a pure phpDoc solution is perfectly fine for my use case.
The only other workaround I can come up with is having to write a separate Results class for each entity type, which would prove to be exhausting.
I don't think there's a way to do exactly what you described, but in similar cases I would suggest using a proxy class for each Results type and document proper return types using phpDocumentor's #method. This solution has added value of having a great place for any type specific Results modifications and expansions. Here's an example:
abstract class Results
{
function all(): array
{
}
function first()
{
}
function paginate(int $perPage, int $pageNo = 1): array
{
}
}
class User { }
/**
* #method User[] all()
* #method User first()
* #method User[] paginate(int $perPage, int $pageNo = 1)
*/
class UserResults extends Results { }
class UserQuery
{
/**
* #var UserResults
*/
private $results;
public function __construct()
{
$this->results = new UserResults();
}
public function results(): UserResults
{
return $this->results;
}
}
$userQuery = new UserQuery();
$test = $userQuery->results()->all();
I have a class to manipulate objects with defined interface
class TaskManager
{
/**
* #param TaskInterface $task
* #param string $command
* #return TaskInterface
*/
public static function editTask($task, $command)
{
$task->setStatus(TaskInterface::TASK_STATUS_ACTIVE);
$task->setCommand($command);
$task->taskSave();
return $task;
}
}
I can create single object by passing its instance as an method argument. This is pretty straightforward. But how should I create many of them?
public static function export()
{
$commands = self::getCommandsToAdd();
foreach($commands as $c){
//This is wrong.
$task = new TaskInterface();
$task->setCommand($c);
$task->save();
//don't need to return it if it's saved
}
}
I can't create it this way. And it's obviously a bad idea to pass array of new objects. Another way is to pass a class name as a string and call its method to retrieve new object. But it seems wrong as well
I think I figured out a solution by myself. Can use a factory interface to pass a factory object.
interface TaskFactoryInterface
{
public static function createNew();
}
/**
* #param TaskFactoryInterface $task_factory
*/
public static function export($task_factory)
{
$commands = self::getCommandsToAdd();
foreach($commands as $c){
$task = $task_factory::createNew();
$task->setCommand($c);
$task->save();
//don't need to return it if it's saved
}
}
What do you think?
I might have implementing this wrong as I cannot figure out a solid way of setting which strategy to use in my implementation of the Strategy Pattern. I'm not a big fan of writing it "statically", perhaps there is another way.
Backstory: I've done two (2) implementations (soap + http) to shipping providers in order to retrieve Track & Trace information for whatever the user inputs frontend. They each follow an interface so that I know which functions are and should (PHP :3) be available. I've shortened the class names below as this is Magento and class names are very long.
Flow: Customer inputs tracking number in a form and submits. Request is sent to the controller, controller initializes an instance of Service class, sets output via. $service->setOutput('tracking/service_gls') - note that tracking/service_gls just maps directly to the service class (Magento thing), $service->getDeliveryInformation($number) is called (we know this exists because of the interface), the entire $service object is returned to the view and data is presented.
My challenge: I'm using a switch case to set tracking/service_gls and tracking/service_otherservice then calling getDeliveryInformation(). Is this the correct approach? I feel it's a bit too static and hard to maintain if someone wants to connect another shipping provider. They would have to enter the controller and manually add another entry to the switch case, in a function somewhere 200 lines deep in the class.
Example of how the controller looks:
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
// SO note: same as doing new Class()
$service = Mage::getModel('tracking/service');
switch ($serviceType)
{
case 'gls':
$service->setOutput('tracking/service_gls');
break;
case 'other':
$service->setOutput('tracking/service_other');
break;
}
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
Code has been shortened a bit as there are lots more functions, but not important.
Interface the two shipping provider models are implementing
interface Output
{
/* Requests delivery information for the specified tracking number */
public function getDeliveryInformation($number);
/**
* Returns acceptor name
* #return string
*/
public function getAcceptorName();
}
Service class handles requesting data from the shipping models
class Service
{
protected $output;
/**
* Sets the output model to use
* #param string $outputType
*/
public function setOutput($outputModel)
{
// SO note: same as doing new Class()
// Magento people note: getModel() works fine tho.. ;-)
$modelInstance = Mage::app()->getConfig()->getModelInstance($outputModel);
$this->output = $modelInstance;
}
/**
* Returns delivery information for the specified tracking number
* #param string $number
* #return instance of output class
*/
public function getDeliveryInformation($number)
{
// SO note: This makes the shipping class request
// information and set data internally on the object
$this->output->getDeliveryInformation($number);
return $this->output;
}
}
Example of a shipping class; I have two in this case
class Service_Gls implements Output
{
const SERVICE_NAME = 'GLS';
const SERVICE_URL = 'http://www.gls-group.eu/276-I-PORTAL-WEBSERVICE/services/Tracking/wsdl/Tracking.wsdl';
protected $locale = 'da_DK';
/* Class constructor */
public function __construct() { }
/**
* Requests delivery information for the specified tracking number
* #param mixed $number
*/
public function getDeliveryInformation($number)
{
$this->_getDeliveryInformation($number);
}
/**
* Requests and sets information for the specified tracking number
* #param mixed $number
*/
private function _getDeliveryInformation($number)
{
// SO note: Extending from Varien_Object has magic __get, __set .. hence why there is no getData() function in this class.
if (!count($this->getData()))
{
$client = new SoapClient($url);
$client->GetTuDetail($reference));
.. set data
}
}
/**
* Returns acceptor name
* #return string
*/
public function getAcceptorName()
{
$signature = $this->getSignature();
return (isset($signature)) ? $this->getSignature() : false;
}
/**
* Returns the name of the current service
* #return string
*/
public function __toString()
{
return self::SERVICE_NAME;
}
}
Controller
class AjaxController extends Mage_Core_Controller_Front_Action
{
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
$service = Mage::getModel('tracking/service');
switch ($serviceType)
{
case 'gls':
$service->setOutput('tracking/service_gls');
break;
case 'other':
$service->setOutput('tracking/service_other');
break;
}
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
}
Well you either do it with a switch or with some sort of string concatenation to return the strategy class you need.
With the Strategy Pattern, choosing the correct strategy at run time is usually done through a StrategyContext pattern: https://sourcemaking.com/design_patterns/strategy/php . This allows you to isolate the algorithm to choose the correct strategy so it is not "in a function somewhere 200 lines deep in the class." .
As to the algorithm for setting the runtime strategy, personally I am a fan of class constants rather than string manipulation etc. Since the aim of the game is to arrive at a class name to instantiate, why not just a class constant to return the class name.
class OutputStrategyContext{
const SERVICE = 'tracking/service_gls';
const OTHER = 'tracking/service_other';
private $strategy;
public function __construct($serviceType)
{
$strategy = constant('self::' . strtoupper($serviceType));
$modelInstance = Mage::app()->getConfig()->getModelInstance($strategy);
$this->strategy = $modelInstance;
}
public function getStrategy()
{
return $this->strategy;
}
}
Lightweight and easy to maintain, the list of strategy classes is in one place.
You can of course make the whole thing static, or use another design pattern like an abstract factory method to acheive the same thing. Up to you really.
Anyway in the controller it is a one-liner
class AjaxController extends Mage_Core_Controller_Front_Action
{
public function getDeliveryInformationAction()
{
$id = $this->getRequest()->getParam('id', false);
if ($id && $this->getRequest()->isAjax())
{
// SO note: service parameter is just two radio buttons with values "gls", "otherservice"
$serviceType = $this->getRequest()->getParam('service', false);
try
{
$service = Mage::getModel('tracking/service');
$outputModel = new OutputStrategyContext($serviceType)->getStrategy();
$service->setOutput($outputModel);
$shipment = $service->getDeliveryInformation($id);
$output = // .. create block that contains the view, $output will contain the shipment data; this is returned to the ajax request.
}
catch (Exception_RequestError $e)
{
..
}
// finally
$this->getResponse()->setHeader('content-type', 'text/html', true);
$this->getResponse()->setBody($output);
}
}
}
Of course you have to modify the service . I also modified my context class for your code.
class Service
{
protected $output;
/**
* Sets the output model to use
* #param string $outputType
*/
public function setOutput($outputModel)
{
// SO note: same as doing new Class()
// Magento people note: getModel() works fine tho.. ;-)
$this->output = $outputModel;
}
/**
* Returns delivery information for the specified tracking number
* #param string $number
* #return instance of output class
*/
public function getDeliveryInformation($number)
{
// SO note: This makes the shipping class request
// information and set data internally on the object
$this->output->getDeliveryInformation($number);
return $this->output;
}
}
In applying the Data Mapper pattern, the model (Domain Model in my case) is responsible for business logic where possible, rather than the mapper that saves the entity to the database.
Does it seem reasonable to build a separate business logic validator for processing user-provided data outside of the model?
An example is below, in PHP syntax.
Let's say we have an entity $person. Let's say that that entity has a property surname which can not be empty when saved.
The user has entered an illegal empty value for surname. Since the model is responsible for encapsulating business logic, I'd expect $person->surname = $surname; to somehow say that the operation was not successful when the user-entered $surname is an empty string.
It seems to me that $person should throw an exception if we attempt to fill one of its properties with an illegal value.
However, from what I've read on exceptions "A user entering 'bad' input is not an exception: it's to be expected." The implication is to not rely on exceptions to validate user data.
How would you suggest approaching this problem, with the balance between letting the Domain Model define business logic, yet not relying on exceptions being thrown by that Domain Model when filling it with user-entered data?
A Domain Model is not necessarily an object that can be directly translated to a database row.
Your Person example does fit this description, and I like to call such an object an Entity (adopted from the Doctrine 2 ORM).
But, like Martin Fowler describes, a Domain Model is any object that incorporates both behavior and data.
a strict solution
Here's a quite strict solution to the problem you describe:
Say your Person Domain Model (or Entity) must have a first name and last name, and optionally a maiden name. These must be strings, but for simplicity may contain any character.
You want to enforce that whenever such a Person exists, these prerequisites are met. The class would look like this:
class Person
{
/**
* #var string
*/
protected $firstname;
/**
* #var string
*/
protected $lastname;
/**
* #var string|null
*/
protected $maidenname;
/**
* #param string $firstname
* #param string $lastname
* #param string|null $maidenname
*/
public function __construct($firstname, $lastname, $maidenname = null)
{
$this->setFirstname($firstname);
$this->setLastname($lastname);
$this->setMaidenname($maidenname);
}
/**
* #param string $firstname
*/
public function setFirstname($firstname)
{
if (!is_string($firstname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->firstname = $firstname;
}
/**
* #return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* #param string $lastname
*/
public function setLastname($lastname)
{
if (!is_string($lastname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->lastname = $lastname;
}
/**
* #return string
*/
public function getLastname()
{
return $this->lastname;
}
/**
* #param string|null $maidenname
*/
public function setMaidenname($maidenname)
{
if (!is_string($maidenname) or !is_null($maidenname)) {
throw new InvalidArgumentException('Must be a string or null');
}
$this->maidenname = $maidenname;
}
/**
* #return string|null
*/
public function getMaidenname()
{
return $this->maidenname;
}
}
As you can see there is no way (disregarding Reflection) that you can instantiate a Person object without having the prerequisites met.
This is a good thing, because whenever you encounter a Person object, you can be a 100% sure about what kind of data you are dealing with.
Now you need a second Domain Model to handle user input, lets call it PersonForm (because it often represents a form being filled out on a website).
It has the same properties as Person, but blindly accepts any kind of data.
It will also have a list of validation rules, a method like isValid() that uses those rules to validate the data, and a method to fetch any violations.
I'll leave the definition of the class to your imagination :)
Last you need a Controller (or Service) to tie these together. Here's some pseudo-code:
class PersonController
{
/**
* #param Request $request
* #param PersonMapper $mapper
* #param ViewRenderer $view
*/
public function createAction($request, $mapper, $view)
{
if ($request->isPost()) {
$data = $request->getPostData();
$personForm = new PersonForm();
$personForm->setData($data);
if ($personForm->isValid()) {
$person = new Person(
$personForm->getFirstname(),
$personForm->getLastname(),
$personForm->getMaidenname()
);
$mapper->insert($person);
// redirect
} else {
$view->setErrors($personForm->getViolations());
$view->setData($data);
}
}
$view->render('create/add');
}
}
As you can see the PersonForm is used to intercept and validate user input. And only if that input is valid a Person is created and saved in the database.
business rules
This does mean that certain business logic will be duplicated:
In Person you'll want to enforce business rules, but it can simple throw an exception when something is off.
In PersonForm you'll have validators that apply the same rules to prevent invalid user input from reaching Person. But here those validators can be more advanced. Think off things like human error messages, breaking on the first rule, etc. You can also apply filters that change the input slightly (like lowercasing a username for example).
In other words: Person will enforce business rules on a low level, while PersonForm is more about handling user input.
more convenient
A less strict approach, but maybe more convenient:
Limit the validation done in Person to enforce required properties, and enforce the type of properties (string, int, etc). No more then that.
You can also have a list of constraints in Person. These are the business rules, but without actual validation code. So it's just a bit of configuration.
Have a Validator service that is capable of receiving data along with a list of constraints. It should be able to validate that data according to the constraints. You'll probably want a small validator class for each type of constraint. (Have a look at the Symfony 2 validator component).
PersonForm can have the Validator service injected, so it can use that service to validate the user input.
Lastly, have a PersonManager service that's responsible for any actions you want to perform on a Person (like create/update/delete, and maybe things like register/activate/etc). The PersonManager will need the PersonMapper as dependency.
When you need to create a Person, you call something like $personManager->create($userInput); That call will create a PersonForm, validate the data, create a Person (when the data is valid), and persist the Person using the PersonMapper.
The key here is this:
You could draw a circle around all these classes and call it your "Person domain" (DDD). And the interface (entry point) to that domain is the PersonManager service. Every action you want to perform on a Person must go through PersonManager.
If you stick to that in your application, you should be safe regarding to ensuring business rules :)
I think the statement "A user entering 'bad' input is not an exception: it's to be expected." is debatable...
But if you don't want to throw an exception, why don't you create an isValid(), or getValidationErrors() method?
You can then throw an exception, if someone tries to save an invalid entity to the database.
Your domain requires that when creating a person, you will provide a first name and a surname. The way I normally approach this is by validating the input model, an input model might look like;
class PersonInput
{
var $firstName;
var $surname;
public function isValid() {
return isset($this->firstName) && isset($this->surname);
}
}
This is really a guard, you can put these rules in your client side code as well to try and prevent this scenario, or you can you return from your post with an invalid person message. I don't see this as an exception, more along the lines of "to be expected" which is why I write the guard code. Your entry into your domain now might look like;
public function createPerson(PersonInput $input) {
if( $input->isValid()) {
$model->createPerson( $input->firstName, $input->surname);
return 'success';
} else {
return 'person must comtain a valid first name and surname';
}
}
This is just my opinion, and how I go about keeping my validation logic away from the domain logic.
I think your design in which the $person->surname = ''; should raise an error or exception could be simplified.
Return the error once
You dont want to catch errors all the time when assigning each value, you want a simple one-stop solution like $person->Valididate() that looks at the current values. Then when you'd call a ->Save() function, it could automatically call ->Validate() first and simply return False.
Return the error details
But returning False, or even an errorcode is often not sufficient: you want the 'who? why?' details. So lets use a class instance to contain the details, i call it ItemFieldErrors. Its passed to Save() and only looked at when Save() returns False.
public function Validate(&$itemFieldErrors = NULL, $aItem = NULL);
Try this complete ItemFieldErrors implementation. An array would suffice, but i found this more structured, versatile and self-documenting. You could always pass and parse the error details more intelligently anywhere/way you like, but often (if not always..) just outputting the asText() summary would do.
/**
* Allows a model to log absent/invalid fields for display to user.
* Can output string like "Birthdate is invalid, Surname is missing"
*
* Pass this to your Validate() model function.
*/
class ItemFieldErrors
{
const FIELDERROR_MISSING = 1;
const FIELDERROR_INVALID = 2;
protected $itemFieldErrors = array();
function __construct()
{
$this->Clear();
}
public function AddErrorMissing($fieldName)
{
$this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_MISSING);
}
public function AddErrorInvalid($fieldName)
{
$this->itemFieldErrors[] = array($fieldName, ItemFieldErrors::FIELDERROR_INVALID);
}
public function ErrorCount()
{
$count = 0;
foreach ($this->itemFieldErrors as $error) {
$count++;
}
unset($error);
return $count;
}
public function Clear()
{
$this->itemFieldErrors = array();
}
/**
* Generate a human readable string to display to user.
* #return string
*/
public function AsText()
{
$s = '';
$comma = '';
foreach($this->itemFieldErrors as $error) {
switch ($error[1]) {
case ItemFieldErrors::FIELDERROR_MISSING:
$s .= $comma . sprintf(qtt("'%s' is absent"), $error[0]);
break;
case ItemFieldErrors::FIELDERROR_INVALID:
$s .= $comma . sprintf(qtt("'%s' is invalid"), $error[0]);
break;
default:
$s .= $comma . sprintf(qtt("'%s' has unforseen issue"), $error[0]);
break;
}
$comma = ', ';
}
unset($error);
return $s;
}
}
Then ofcourse there is $person->Save() that needs to receive it so it can pass it through to Validate(). In my code, whenever i 'load' data from the user (form submission) the same Validate() is called already, not only when saving.
The model, would do this:
class PersonModel extends BaseModel {
public $item = array();
public function Validate(&$itemFieldErrors = NULL, $aItem = NULL) {
// Prerequisites
if ($itemFieldErrors === NULL) { $itemFieldErrors = new ItemFieldErrors(); }
if ($aItem === NULL) { $aItem = $this->item; }
// Validate
if (trim($aItem['name'])=='') { $itemFieldErrors->AddErrorMissing('name'); }
if (trim($aItem['surname'])=='') { $itemFieldErrors->AddErrorMissing('surname'); }
if (!isValidDate($aItem['birthdate'])) { $itemFieldErrors->AddErrorInvalid('birthdate'); }
return ($itemFieldErrors->ErrorCount() == 0);
}
public function Load()..
public function Save()..
}
This simple model would hold all data in $item, so it simply exposes fields as $person->item['surname'].
Currently I am trying to learn the Zend Framework and therefore I bought the book "Zend Framework in Action".
In chapter 3, a basic model and controller is introduced along with unit tests for both of them. The basic controller looks like this:
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$this->view->title = 'Welcome';
$placesFinder = new Places();
$this->view->places = $placesFinder->fetchLatest();
}
}
Places is the model class that fetches the latest places from the database. What bugs me here: how should I test the IndexController in isolation? As the reference to the Places class is "hardcoded", I cant inject any stubs or mocks in IndexController.
What I would rather like to have is something like this:
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();
}
}
The first code sample I posted is most definately NOT unit test friendly as IndexController cannot be tested in isolation. The second one is much better. Now I just need some way to inject the model instances into the controller objects.
I know that the Zend Framework per se has no component for dependency injection. But there are some good frameworks out there for PHP, can any be used together with Zend Framework? Or is there some other way to do this in Zend Framework?
Logic to models
First of all, it's worth to mention, that controllers should need only functional tests, though all the logic belongs to models.
My implementation
Here is an excerpt from my Action Controller implementation, which solves the following problems:
allows inject any dependency to actions
validates the action parameters, e.g. you may not pass array in $_GET when integer is expected
My full code allows also to generate canonical URL (for SEO or unique page hash for stats) based or required or handled action params. For this, I use this abstract Action Controller and custom Request object, but this is not the case we discuss here.
Obviously, I use Reflections to automatically determine action parameters and dependency objects.
This is a huge advantage and simplifies the code, but also has an impact in performance (minimal and not important in case of my app and server), but you may implement some caching to speed it up. Calculate the benefits and the drawbacks, then decide.
DocBlock annotations are becoming a pretty well known industry standard, and parsing it for evaluation purposes becomes more popular (e.g. Doctrine 2). I used this technique for many apps and it worked nicely.
Writing this class I was inspired by Actions, now with params! and Jani Hartikainen's blog post.
So, here is the code:
<?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();
}
}
To use this, just:
Your_FineController extends Your_Controller_Action {}
and provide annotations to actions, as usual (at least you already should ;).
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 is required, however I use Netbeans IDE, so the DocBlock is automatically generated based on action arguments)
Ok, this is how I did it:
As IoC Framework I used this component of the symfony framework (but I didnt download the latest version, I used an older one I used on projects before... keep that in mind!). I added its classes under /library/ioc/lib/.
I added these init function in my Bootstrap.php in order to register the autoloader of the IoC framework:
protected function _initIocFrameworkAutoloader()
{
require_once(APPLICATION_PATH . '/../library/Ioc/lib/sfServiceContainerAutoloader.php');
sfServiceContainerAutoloader::register();
}
Next, I made some settings in application.ini which set the path to the wiring xml and allow to disable automatic dependency injection e. g. in unit tests:
ioc.controllers.wiringXml = APPLICATION_PATH "/objectconfiguration/controllers.xml"
ioc.controllers.enableIoc = 1
Then, I created a custom builder class, which extends sfServiceContainerBuilder and put it under /library/MyStuff/Ioc/Builder.php. In this test project I keep all my classes under /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);
}
}
}
Last, I created a custom controller class in /library/MyStuff/Controller.php which all my controllers inherit from:
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);
}
}
What this basically does is using the IoC Framework in order to initialize the already created controller instance ($this). Simple tests I did seemed to do what I want... let´s see how this performs in real life situations. ;)
It´s still monkey patching somehow, but the Zend Framework doesn´t seem to provide a hook where I can create the controller instance with a custom controller factory, so this is the best I came up with...
I'm currently working on the same question, and after deep research I've decide to use Symfony Dependency Injection component. You can get good info from official website http://symfony.com/doc/current/book/service_container.html.
I've build custom getContainer() method in bootstrap, which resturns now service container, and it simply can be used in controllers like
public function init()
{
$sc = $this->getInvokeArg('bootstrap')->getContainer();
$this->placesService = $sc->get('PlacesService');
}
Here you can find how to do that http://blog.starreveld.com/2009/11/using-symfony-di-container-with.html. But I changed ContainerFactory, because of using Symfony2 component, instead of first version.
You could also just use the PHP-DI ZF bridge: http://php-di.org/doc/frameworks/zf1.html
I know this question is really old but it pops up rather high in search engines when looking for DI in ZF1 so I thought I'd add a solution that doesn't require you to write it all by yourself.
With the Service Manager at Zend Framework 3.
Official Documentation:
https://zendframework.github.io/zend-servicemanager/
Dependencies at your Controller are usually be injected by DI Constructor injector.
I could provide one example, that inject a Factory responsible to create the ViewModel instance into the controller.
Example:
Controller
`
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);
}
}
`
Complete example at https://github.com/fmacias/SMSDispatcher
I hope it helps someone