I have been thinking a bit recently how to manage dependencies and how to structure Zend Framework based applications to make the code less coupled, more testable and less dependent on the global scope.
I don't mean to be negative but I am not too happy about the web application structure that most articles and books present. In Zend Framework world controller seems to be the place when things get done. Controller is the workhorse and this is where all the logic seems to be buried. It also seems to me that model in MVC is reduced to database integration but there is no services layer for some reason. Where ever you look you will see the same examples with controller doing all the work and models being simple Zend_Db_Table or Zend_Db_Table_Row instances. You will not see business logic focused classes, Controller or DB Model, thats all you can choose from.
The problem I see with this solution is that controllers are not reusable. Controller action is coupled too tightly with way too many things to be reusable. You can not call controller action from a cron or a REST server. You can not easily reuse controller action from a queue processor. Why? Because it depends on session, request, cookies, response, views etc. Controller has way to many things on its mind, if you want to reuse it you would have to satisfy all its dependencies which get out of control easily.
What you can see in the wild is even worse. People use $_POST and pass it around to the view or models. In result everything depends on everything and you have a true spaghetti code.
So how to decouple components in Zend Framework applications?
The first thing that we should aim for is to reduce the amount of code that is coupled with Zend Framework itself and especially with the request scope. We should try to create decoupled services and POPO (Plain Old PHP Objects) which handle business logic. They should not be aware of session nor request/response/cookies etc. They should depend solely on what is provided in constructor and method arguments.
The less class depends on the better. If class depends on a cookie and DB that is already too much as it means that the class is tightly coupled with both the presentation and database. In multi layer architecture you should not allow lower layers to call or depend on higher layers. You should also not allow direct use of bottom layers by top layers etc. You spend more time on to achieve this separation but the benefits are huge as you end up with a system that is structured and maintenable.
After giving it some thought I came up with the following diagram of how dependencies and scopes could look like in a Zend Framework based application.
There is a lot of rules here represented mostly by the dashed arrows.
Service is king
In this utopia-like diagram services are where the job gets done. Services present a simple API to perform all the complex operations behind it. It can be anything, for example:
$userService->disable($user) $captchaService->generate() $purchaseService->completeOrder($order) $cache->load($key)
The point is that services are kept away from the following:
- current user context (beside permissions)
- request parameter names (associative arrays containing POST are not cool)
This means that service class has a well defined API. It exposes simple methods with parameters that have structure (interfaces or classes not associative arrays).
It also means that services can depend on each other and on the external libraries and/or database. Here I would divide services to two categories. Database aware and database agnostic. The difference is important from testing and reuse point of view. If your class depends on tables and db structure you wont be able to reuse it or test in isolation easily. If your service depends only on the interfaces injected in the constructor then you can easily replace them with mocks or wire the application differently at runtime.
Last important thing about services is that they do not keep state. You should expect a service to do the job and after service method returns something service should be ready to take another request. Then it does not matter if you want to process one payment or a queue of million payments. Your payment service takes a payment object as argument and processes it. It can succeed or fail but it should still be ready to take another payment without aftereffects. It is also very important if you want to provide REST or SOAP API for your application. Having stateless services you can easily invoke them from your API layer or crons and you will not need any modifications.
Services are classes that perform operations based only on the service method arguments. If we create services this way they can be easily invoked from controllers, plugins, command line, queue or Soap entry points. They are also much easier to test as we know they only depend on the arguments (an constructor injected services/objects).
Database aware/agnostic services, why should we care?
On the diagram I separate them to highlight that dependencies on database are heavy. To make these services simpler we do not allow them to depend on 3rd party code directly or talk to external services. If a DB-aware service needs to generate a PDF it should use a PDF service. Then code of DB aware service stays simpler and PDF service takes responsibility of PDF generation (which is not trivial).
Please note that there is no dependency in the opposite direction. Paypal service facade does not call the database-aware service. It should not. If PayPal needs to expose endpoint for payment notifications it should be a controller calling a payment service which in turn would ask PayPal service to process the notification (extract reference id and status).
What can models do?
Starting from the bottom we see models talking to the database. They have no idea what is a request, they are not allowed to call services nor controllers. They can not use external libraries nor session, cookies etc. They should not be coupled to request parameters either. Models should be pretty dumb as they all extend Zend_Db_XX so reuse will be limited. I think it is ok to let models depend on other models and let them perform different queries.
How to incorporate 3rd party code?
In left bottom corner I placed external libraries as I think it is important to keep them in mind.
Every application uses some 3rd party code. If we want to keep our application safe from external changes we should separate it with a wrapper (facade) service. We never know what bugs are there and if we won't have to replace the implementation. We do not want to pollute our API with 3rd party standards and exceptions either.
The best way would be to wrap external libraries in form of simplified services. These services simply delegate to 3rd party code or perform operations to hide complexity. Interfaces that expose exactly and only what we need. If we want to resize images lets create a facade with simple API to resize images. Our application does not have to create any external instances, it is separated from implementation details and it is easier for us to change the behavior if necessary as its all in the image resizing service.
Services that encapsulate 3rd party code components should not be aware of request/response/session/database either. They should focus on being adapters between 3rd party code and our service interface.
How to integrate with external systems?
In the same layer of services we provide services that talk to external systems. Again they provide a function of adapters as they would not have any major business logic.
How to support crons, SOAP, command line tasks and Queues?
Now that we already have a services layer. It is very easy for us to add a queue processor that populates service arguments with queue data and executes a service method. We can add SAOP endpoint with methods that map SOAP arguments to our service method arguments and we should be ready to go. Crons are equally easy to implement. We can have complex tasks reused across web/soap/cron interfaces.
The VC related code (view and controller)
On the very top of the diagram we have a bunch of different components. We have Front Plugins, Controllers, Action Helpers, View Helpers, Forms, Views and Partial Views. It is quite a lot so lets break it down.
I think front plugins can not escape coupling to request, cookies and session. Front plugins can be used for stuff like redirecting, geo location, permissions etc. In many cases these components will depend on request, session and cookies. Plugins can also be involved in preparing user context and permissions.
Front plugins do not really touch the views but their life cycle is controlled by MVC and is quite closely related to controllers so I put them in the same group of view related classes.
There is no controversy about the role of controllers any more I guess. They are the bridge between user interface and services. Controllers use request, response, cookies and session to assemble service methods' arguments. Controllers do not contain real business logic. They do not talk to Zend_Db_XX classes, they do not use 3rd party code etc.
Controllers are using forms and populating view parameters. Controller actions can also use action helpers.
Views and partial views
I think this area is safe and simple too. Views should only access values populated by controllers and view helpers. View files should never access session, cookies, request parameters nor invoke service method. They render the user interface and this is their only responsibility.
Action helpers and View helpers
Now for the more controversial components, Action helpers and View helpers. I think action helpers are code that should be reused across controller actions but still depends on request / response / session / cookies. We should not have a lot of it but I guess in any application there will be components like this so better to encapsulate the similarity then copy paste.
I am not sure which components would fit best into action helpers. Should they be allowed to use forms? On the diagram I did not draw the line suggesting that action helpers can not use forms. I am also not sure yet should action helpers be allowed to set view parameters? I think they should not, as it would be difficult to keep track of which action helper sets which values and what has to be called to make sure that view has all the variables it needs.
View Helpers are the biggest controversy here so lets spend some more time on it.
View helpers, smart or dumb?
Looking at different frameworks you will find that view helpers are usually just simple HTML preprocessors. Usually they look very ugly having a lot of HTML generation inside of the helper method.
But where do we put standalone components in Zend Framework MVC? If I want to have a user login bar at the top of every page should I populate partial view variables in init() method of base controller? Do I have to call action helper or set view variables in every action?
What if I want to have a refer a friend widget on some of the pages? Do I have to add variables into my view in every action? Would it not be easier to use a view helper instead and treat it as a component? Would it not be nice to say “give me a featured product here” in the layout or some view and have the recommended product details loaded for you?
This issue is probably the only thing I am not really happy about when it comes to MVC frameworks like Zend Framework. There is no solution to my problem. I want a mini MVC component with its own life cycle. I want to be able to decide in the view should it be evaluated or not. View helpers fit quite well into this use case. I assemble service method arguments, call service, get response, populate partial view and render it. Job done, featured product is on the page.
Maybe it could be done on a layout level somehow? I am not sure if it has to be in the view helper but I it seems flexible.
I am not really sure if this is a best way to do it but I think view helpers should be allowed to do simple controller-like tasks and ask services for some data if necessary to render the widget. It may be a call to get user's flicker preferences to show the right images in the side bar or it can be a query to generate menu tree based on permissions and preferences. I believe that as long as the service is doing all the heavy work and helper just consumes it then we should be fine.
Purists will say it is an violation of MVC, but MVC is not a web pattern. It was designed for desktop applications and the Web twist on MVC is quite different from the original pattern. When you think of it and look at the diagrams you will see arrows from view to model. Zend calls models db tables and rows, our models are much more sophisticated, our models are services. So my gut feeling tells me that allowing view helpers to access service methods is fine.
The last piece of the puzzle is the user context and translation which have to be available in all the view related code. We will make decisions in the view based on permissions (show or hide option). We will make decisions based on user profile, preferences etc all the time so having a well defined user context available in VC makes sense.
I believe services should not be aware of the user to keep them simple, if you want to incorporate permissions into service logic you will have to come up with an actor parameter that is passed around to every service call or make it available somehow via static scope. But then you make your services more coupled to the user and it may be harder to test them in isolation or use them from user-less scope like cron or message queue.
I think it would make sense to prepare user context in the front plugin or somewhere like this and decorate the request object to make user data available across VC layer. Your user object could have whatever is commonly used:
- personal details
I would not make the user object responsible for talking to services though. I think it should be a data container for VC layer to grab the basic information from.
Well the article got a bit longer than i intended but i hope i did not bore you to death :) Please let me know what do you think and share some thoughts with me. I am especially interested in your experiences with services and independent components.