[symfony workshop] How to implement a flexible home page composition admin tool with LooseCoupling?
My employer is a big magazine publishing house and in the three years working there I had the opportunity to work on or just follow the development of more than 20 editorial websites. These included very different types of sites; some community driven, some purely editorial, some service providing and more.
There have been many differen requirements but most of the time the requested features were reoccuring in similar ways.
One of most frequently requested feature was to provide an admin tool to compose homepage (a.k.a. indexpages, sectionhomes, channels,..). An editor should be able to compose the layout of a homepage by positioning design elements like top teasers, slideshows, two column boxes, three column boxes and so forth and furthermore he should be able to manually assign contents to those elements. These contents of course can be of various types like article, galleries, doissiers, quizzes, etc..
This is my approach to solve this requirement once and for all!
There are at least two difficult questions to answer.
- When all content elements can be of various types in any order then how can we manage the relations and at the same time maintain a minimum number of queries to the database?
- How can we create an easy to use backend for this?
But lets start from the beginning.
To make this post a bit more lively I created three different types of contents.
I am sure you can imagine a lot more types of contents and in fact I’ve seen hundrets of different types of contents.
The design elements
If I think about a homepage I think of a list of design elements or blocks starting from a top teaser, medium teaser, small teaser, two column teaser, slideshow, gallery teaser, glossary teaser and much more. All these elements can be put in any order to result in the layout of the homepage in question. Sometimes these elements need to be automatically populated with the latest, most popular or search matching contents but often they need to be populated manually because editors are very keen on control and won’t leave their precious homepage to a robot.
Here’s a list of design elements. I define them in the app.yml and give each one a name and a component or partial.
Since a design element is always just a part of a website it makes sense to implement them as partials or if necessary components.
Is is also important to configure what kind of contents are supported by this element as it probably doesn ‘t make sense to assign a quiz to a slideshow.
The page itself
Lets start with a very simplistic schema for the page.
We want to define only the title for now but later on there will probably be more to it such as an attribute for the layout template, for a sidebar and a lot more.
The missing links
The link between the page, the design elements and the contents is still missing so this would be the next step to define. Lets start with the assignment of design elements to the page.
There can be no relation for this as the design elements so far are only partials and components as well as names in our app.yml but it is exactly this app.yml configuration that I want to use. So I came up with the following schema definition:
You can see that is has a has-many relation to the page which is why it also needs a position attribute so we know in which order they need to be displayed. The elementName column corresponds with the setting in the app.yml so it is easy to know what partial or component is going to be used without yet another join to yet another table.
In the admin module I would like to be able to just drag and drop some element placeholder images onto a canvas of the current page but for now I’d like to keep it simple and create DesignElement instances in a dedicated module.
Now it’s time to build our page action which will fetch all DesignElement instances for the current page and output them using include_partial() and include_component().
If you plan on having fully automatic design element that always show “the latest” of something or “the most popular” or anything else automatic rather than manual you can now proceed to implement that logic in the appropriate DesignElements component.
The manual assignment contents
For all non- or semi-automatic DesignElements there has to be a way to assign contents manually.
For example if we have a DesignElement that displays three small teasers with an image, a headline and a text excerpt each we need a way to link the appropriate contents to this element.
As these contents can be of many types (just as we configured in the app.yml) there can be no doctrine relation. However we can use the LooseCoupling extension for Doctrine (actually I created it with exactly this purpose in mind).
The LooseCoupleable behaviour will add two more columns obj_type and obj_pk to this model which are used to loosely relate any kind of record. See also my last post about its concept or have a look at the sources on my GitHub repository.
Ok next we really need a godd and usable backend module to do some fancy drag and drop action. However this is just too much to do for this post so I made up some fixtures to support at least the frontend part. Here are some simple fake contents.
And here are some fixtures for a simple homepage setup.
If you review the app.yml settings for the slideshow element again you will see that the value for render is an array instead of a string. That’s my convention for using a component rather than a partial. In case of a slideshow I think about collecting the images automatically.
The last thing we need for displaying our design elements on the page is a page action (in a plugin module in my case).
And then we need a template for it.
As you can see we iterate over the design elements and render them using partials or components depending on the settings in the app.yml.
Of course now I have to create all the modules for the slideshow and the teasers but you won’t need an example for that I reckon.