The example problem is how to get a reference to a MailService instance for executing this code inside a controller:
$user->sendWelcomeEmail($mailService);where $user is an istance of the User entity class, thus having no injected services and requiring one to be passed as an argument to the function. This problem was proposed by @apinstein.
I can think of several solutions, and others have been proposed by @beberlei and @weierophinney.
1. Just inject the MailService in the controller: it should ask for the collaborator in the constructor (@mhevery)
This would be correct in Java and it is an example of pure design, but in Zend Framework constructor injection is not available for controllers. They are required to have a no-arguments constructor: this contract leads us to the next solution.
2. Controllers may not have constructors, but there are still ways to inject services/models (@weierophinney).
3. Yadif allows to inject #zf controllers.(@beberlei)
Yadif is a dependency injection framework for php that implements setter injection. Using Yadif in conjunction with setters prepared on the controllers let you unit test them as you can call the setters in test code to pass in stubs and fake objects.
4. Instance the collaborators as bootstrap resources and leave them hanging there waiting for the controllers to use them.
The bootstrap object acts as a service locator. If I understand this process, and we want to unit test the controllers, we should avoid a service locator as it involves breaking the Law of Demeter (getting the bootstrap object only to get the collaborators), and we would have to prepare the container only to put in services, without the Api of the controller telling us what collaborators a controller depends upon.
Also instancing services that might not be used during every request can be expensive. I don't know if there is a lazy loading capability for bootstrap resources. This option is the worst for testing and maintenance.
5. Push down the logic in the model layer (me)
Controllers in Zend Framework are meant as a thin layer over a large domain model one. There is no problem for me in not having easy injection in them because I would put in the controllers only wiring code, which receives the main factory from the bootstrap, request it to create the domain services which have to do the real work and pass request parameters to them, assigning view variables as a side-effect.
$repository = $this->application->factory->getUserRepository(); $user = new User(...); // with a factory if you prefer $view->message = $repository->registerUser($user);I know this is breaking the Law of Demeter, but the point here is that if the controller is a thin enough layer, which contains no logic, there is no need to unit test it, and experience the pain of preparing stubs that return stubs that return stubs. This code will be exercised in integration tests that have all the same bootstrap, stubbing the main factory to provide fake collaborators only where they are not practical (a fake MailService in this case).
Here are the reason why I like this approach to controllers:
- reusability: controllers code is not reusable, to the point that it is suggested to move much of it in helper classes. I do not want to rewrite logic in different places, so it's better for me to keep business logic in the domain layer.
- simplification: if the controllers are dumb and expose the underlying layers as-is, that would be no translation between the domain model and the end user mental model. The entities presented in the user interface will be the same that live in the domain layer.
- testing: mocking an array of domain parameters is simpler than mocking request objects. Obviously unit testing is performed only in the domain layer.
- abstraction: logic is not technology dependent. If you keep logic in the controllers, you are coupled to Zend Framework, while a domain model is agnostic on every other component of the application, isolated by interfaces. It's like using a DataMapper instead of an ActiveRecord.
In conclusion, I think you should consider all these five practices for improving your controllers design and make sure your business logic is well tested. Choose the solution that works for you.
6 comments:
What I was getting at is that you can grab configuration or explicit resources from the bootstrap. If you opt for storing configuration only, it's relatively trivial to pass that configuration into your service and domain objects -- which then allows them to configure themselves with the appropriate capabilities. Your controllers do not need any special capabilities then, and you can do lightweight integration/QA testing on your controllers to ensure that relevant content is returned from your controllers (which can be important particularly when serving multiple formats from the same actions).
Thanks for the additions Matthew.
Hi, @miskohevery -> @mhevery.
Thanks, fixed.
Zend Framework constructor injection is not available for controllers. They are required to have a no-arguments constructor
ZF doesn't have no-arguments constructors for controllers. Controller takes $request, $response and optional $invokeArguments in constructor. So, controllers are required to have exactly these arguments when instantiated.
You are right, what I intended in the post is that constructor is not available for injection of collaborators since its signature is fixed. I was guessing incorrectly that request and response were injected via setters. :)
Post a Comment