Behind The Scenes: Iterative Design on a New Component of the Recess Framework
Over the last four months Joshua Paine and I have been iterating on the design of a layout & view system for the Recess Framework. The new layout & views are a major part of next week's 0.2 release. Recess is an open-source PHP framework. I know what you're thinking: how much "design" can you really do in PHP? If you can agree that design is the process of taking something raw and making it beautifully functional, then in PHP there's a lot of opportunity for design.
In the initial release of Recess there wasn't much of a story for the front-end, views. You got plain-old PHP, could plug-in Smarty, or roll-your-own. Templating/layout/view APIs are hard to design because they're tremendously important: views and UI dominate the time spent developing most applications. Rather than taking a swing in the dark I decided to see if anyone in the Recess community would step-up to the plate and knock it out of the park. To the benefit of Recess users, Joshua Paine did just that.
This is the behind-the-scenes of how Recess' new templating and layouts came to be, and an interesting look at the evolution of an API.
Design #1 - "View" Helper: The Swiss Army Knife
The original API work was done by Joshua with a helper class called simply 'view' (click above for full example). It invented some abstractions, with inspiration from other libraries, on top of PHP's output buffering: blocks, slots, & templates. The original class had 10 static methods around these abstractions in roughly 90 lines of code. Let's walk-through some key components of the origina ldesign.
"Blocks" & "Slots"
A concept I fell in love with immediately: blocks and slots. Blocks fill slots. Blocks in child templates fill slots in parent layouts. It wasn't too different from other slot-driven layout systems (Django's blocks, Rail's yield, Smyfony's slots, etc) but the blocks fill slots idiom was beautiful. (Aside: Having Blocks in Recess, also a win!) Let's take a look at how they worked:
A Block 'navigation' in a 'Child' Template:
Fills a Slot in a 'Parent' Layout:
The 'navigation' block in the child template becomes a variable,
$navigation, in the parent layout, and is used to fill a slot. If no block is provided we can have default contents.
Do you notice an assymetry here? Blocks are named with strings, and, with some magic, become variables in the parent layout. The jump between
slot($navigation) may feel small, but it was a point we could add symmetry and thus consistency. So we've got two leads on improving the design: 1) Increase Symmetry, 2) Reduce Magic.
A by-product of the magic that turns a block named 'navigation' to a variable named
$navigation in the parent layout is that in order to know what blocks to pass a parent, you must understand the 'guts' of the parent (or parents, with child->section->master). This isn't a big deal when you're writing both concurrently, but when you come back to a project a few months later, or when you're working on a team, it's a real pain with complex layouts. So, we've got another design improvement lead: 3) Don't expose your guts.
If you've used a web framework you're likely familiar with the notion of 'partial' templates. Unlike the Layouts just mentioned that flow from specific to general, Partials flow the opposite direction. In the following example we're rendering the 'person/details' partial template with a $person:
(Note: The comma should be a => for 'key' => $value).
The notion of passing a key/value array to a template in PHP is common practice. In this instance it really hits a nerve. This is an example of PHP puke. To pass variables to a partial template you've now got to know the name and type of the inputs to a partial and match them in a keyed array.
Partial templates, just like functions, take specific inputs. Why can't I call a partial like a function and pass my parameters in linearly? Now, we've got another lead for a design improvement: 4) Partial templates should call-by-contract.
Finally, there were some methods that didn't really seem necessary: if the pattern of use is that child templates always start and end by extending a parent (or don't extend one at all) then we could remove some noise from the API by making a method like end_template called by the framework instead of the user. Our final lead for a design improvement: 5) Reduce noise & concept-count from user code.
Design #2 - Layouts: A Simple Pattern
After spending some time with the original system and noting the potential for design improvements I iterated on the original API. 10 methods on 1 class became 8 methods on 2. When it comes to an API, if you can accomplish the roughly the same tasks, less is more.
The first, big cosmetic change is in naming: 'view' is an overloaded term that already has meaning in Recess so the 'view' helper became the 'Layout' helper. Rather than conflating layouts and partial templates, partials got their own helper class: 'Part'.
Symmetric Slots & Blocks
Let's attack those weak points in the design. In the second iteration, the first three leads for design improvement were addressed with a simple change: slots and blocks are both specified using strings. How did this address each point?
1) Increase symmetry - Now the methods to start slots and blocks both took strings. No more remembering which one took a string and which took a variable.
2) Reduce magic - Variables no longer magically appear in the scope of your parent layout. The only way to access the shared state from a child template is to ask for it from Layout.
3) Don't expose your guts - By requiring parent templates use Layout for every slot, it became easy to look at a layout file and know exactly what blocks a child could pass. You could even use a simple regex to extract the names. Unfortunately, they're still scattered throughout the layout so child template developers still have to wade through some guts. Yes, it's much easier to do now, but it could be better. Here's an example parent layout:
The Assertive Template is Born
How can we make headway on #4) Parts should be call-by-contract? An easy way: define parts as functions. Unfortunately, for view logic this winds up looking pretty ugly in PHP. It also means you have the mental burden of naming a function. Sure, you could use a convention based on the file name, but it's an extra step.
How do functions work? They name their inputs in linear order. How could we name our inputs in linear order? We could assert them and their type. Thus, the assertive template was born. With a simple regex we can know, for certain, what variables and types a part takes. Here's an example:
Here's how we now render it, compare with
view::template('person/details', array('person' => $person));
This is a big win. Though we've increased the cost of implementing a part, we've simplified the common case of actually using the part. We've also hidden some more guts, just like a function, by only needing to know the order and the type of inputs. We've also opened the door to higher-order functionality with full-knowledge of a part's inputs. I'll let you dream about the implications...
Obtuse: Passing Variables with Slots
In the process of improving the design, trade-offs were made. Trade-offs that had unintended consequences. The most obvious was in requiring symmetry with blocks and slots the case where you simply wanted to print a slot became excessive:
So there's room to improve yet, with another lead: #6) Simple shouldn't be hard.
A more subtle, but more damaging consequence of the second design was that passing variables between child and parent wasn't straightforward, passing blocks and slots was. What I failed to realize at the time, but Joshua did immediately, was in bringing symmetry to the great 'blocks fill slots' abstraction complicated the plain-old-PHP. While looking at how I had resolved #5) reduce noise & concept count from user code, by removing the need for template_end in user code (or, now extendEnd) he had an epiphany and another lead for a better design: #7) Slay cute abstractions and simplify, simplify, simplify.
Design #3: Who Needs Slots? Same Scope Minimalism.
From 10 methods to 8 and now to 4. No more slots!? Tomorrow we'll dive into how Joshua turned the problem on it's head and simplified everything. Was it too simple? We'll address this in the 4th and the final design of Recess' view and templates system in 0.20 version launching next week. Stay tuned...