For almost two years, I’ve been developing a membership management system that recently became an open-source project, Cantiga. It was a big come-back for me to PHP world, abandoned five years ago in favor of Java. I must admit that I’m really impressed with the progress made within the PHP community regarding the code quality and approach to the software development. The main horsepower of this movement is French company, Sensio Labs, and their framework, Symfony 2. Thanks guys for making it possible to write normal code in PHP without building everything from scratch on my own.
Of course, this does not mean that Java becomes lingua non grata for me. This is still my #1 programming language. However, there is one thing I miss in it: the simplicity of building website backends. Before Java, I never thought that a simple task of receiving a map of key-value pairs called HTTP request and sending back some generated text can be so complicated. At university, I was taught about application servers, tuning their garbage collectors, stateful UI over a purely stateless technology (!). In my first years of being a professional Java programmer, I fought against 1-MB session states, serialization of the universe (hi, Wicket!), and Guice-HK2 interoperability (hi, Jersey!), thinking about days where things used to be simple. Recently, I’ve playing with Java microframeworks, which are a big step forward, but still need a lot of extra crafting, if you need something more than a microservice from them. I haven’t gave up, it is just a matter of time and learning the tools. But in PHP, I just have it. Setting up Symfony after a couple of years of absence, took me five minutes, and after reading some introductory pages, I knew what to do to have a nice website, with content, forms, some JS… When working on the initial pieces of Cantiga, I had strict deadlines, because otherwise people from the whole country would not be able to sign in for a nationwide event, and its local leaders would not be able to prepare it. So, PHP and Symfony was an obvious choice.
Architecture modernization
After a year of development, and open-sourcing the project, it’s time to review the architecture. My knowledge of the domain is bigger, I see how my initial ideas work in practice and what can be done better. I strongly based on the default Symfony design of controllers:
It worked well at the beginning, but as the number of controllers increased, the design started kicking me over time. Many of them were variations of CRUD, with various customizations (i.e. lack of certain actions, multiplication of some of them). So I added action classes that wrapped the common code and action methods just initialized them. Later, it turned out that I need three exactly the same controllers, doing exactly the same thing, but in a different context. Later, another such issue appeared, and another… so I used traits, and exposed dummy methods in each of the controllers to map the actions to the correct routes, because of the decision to use @Route
annotation. We see clear maintenance problems:
- code reusability - no general solution, promotes copy-pasting,
- certain elements, such as annotations, cannot be moved to the base class - you always need a wrapper action method over the actual one just to add an annotation to it,
- I hid a lot of additional code, such as menu generation, etc. around the controller, in different hooks magically run before and after the action, and it was strictly tied to my controller class hierarchy. You had to take everything even if you did not need many of the things.
Another maintenance problem emerged when I decided to migrate to Symfony 3. It turned out that Symfony developers made all the API exposed by the default Controller
class protected, and all my action classes stopped working, because they relied on the access to the controller object that called them. It forced me to copy-paste pieces of original Symfony code to my base controller class just to make them public again as quickly as possible.
I think that I gave you enough fuel to make you thing that all of this is so wrong. And you’re right, it is. The default design of Symfony is good for simpler use cases, and fast scaffolding, and I’m quite okay with that, because this is why I was able to start writing my app in 5 minutes. Use cases for a different approach could only emerge by trying to write it, so I could not know them in advance (unless I wrote a similar app in the past). The Symfony devs could not know them, too, but the brilliant thing is that they know I might need something different, and allowed me to do so. The framework is open for a completely different controller design, so it is not a problem to provide one.
Direction
My new approach makes use of two features of Symfony:
- your controller can be any valid callable, including anonymous function,
- your controller can be a service, initialized by the dependency injection container.
I looked at my design and noticed that one action per controller class will be much better for me, so I’ll be writing action classes:
My action needs to interact with the framework, and dependency injection solves that, together with a piece of YAML code to wire it:
Since Symfony 3.1, you can create custom argument resolvers for the action methods. Now it is very simple to receive pre-processed data from the universe like this (WorkspaceInfo
is my custom data holder):
This design gives me the following advantages over the previous design:
- clear object-oriented code, with the environment injected, not fetched:
- services - via the constructor,
- context data - via the arguments,
- I can easily re-use an existing action with simple
extends
(no wrapping, traits, etc.) - No way to hide magic - everything must be explicitly specified in the action code.
At the first sight it seems that injecting services and avoiding magic will actually produce more boilerplate code. I don’t agree with this. The rule is simple: if you see that your actions need more than three, four services, it means that you need to enter the higher abstraction level. Find out whether your actions share a general pattern, design an API for it and expose as a service.
What’s next
In the next post, I’ll write more about the services built around the new design of my controllers, that will give us access to the features of Cantiga. However, the idea is general enough that can be used in other projects, as well. Stay tuned! I hope that this time the next post will be much earlier. There was a rather unfortunate sequence of events that prevented me from finishing this post earlier: World Youth Days in Krakow, where I live, that consumed me a lot of time, and right after that I accidentally destroyed my Linux virtual machine with Jekyll, so I had to set everything up from scratch. Shit happens.
Further reading:
- Defining Controllers as Services - Symfony manual,
- How to create framework-independent controllers - great source of ideas for your controller design.