How to structure a Zend Framework application and its dependencies
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 do 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, that’s 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 cannot call
controller action from a cron or a REST server. You cannot easily reuse
controller action from a queue processor. Why? Because it depends on session,
request, cookies, response, views etc. Controller has way too 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.
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 a class depends on a cookie or 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 maintainable.
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 are 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:
1 2 3 4 |
$userService->disable($user) $captchaService->generate() $purchaseService->completeOrder($order) $cache->load($key) |
The point is that
services are kept away from the following:
·
request
·
response
·
session
·
cookies
·
view
·
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 won’t 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 (a constructor injected
services/objects).
Database aware/agnostic services, why should we care?
On the diagram I
separate those 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 cannot 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 it’s 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 let’s break it down.
Front Plugins
I think front plugins cannot
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.
Controllers
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 cannot
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 let’s 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 to 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
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
a 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.
User context
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:
·
permissions
·
userId
·
personal
details
·
preferences
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.
Discussion time
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.
Cheers