Object Rendering or MVC the right way
Last modified 2022/09/14 11:14The templates we use in our favourite MVC frameworks are awful. You pass an unstructured bunch of data to a template which hopes the you passed everything it needed. They do not define what their inputs are, they are not type safe.
For some years I’ve been using a different way of rendering views - it started
when I was trying to create a new CMS framework, but since then it has come in
useful time and time again. I call it Object Rendering
, but it is very
similar to Martin Fowlers Presentation
Model.
Object Rendering ¶
The idea is simple:
- Render a template based on the FQN of an object.
- That object contains all the data for the template.
- The template can render objects from the main object.
So, assuming we do this in a typical web application your controller might look like this:
<?php
class MyController {
public function __construct(private ObjectRenderer $renderer)
{}
public function blogPostAction(string $slug): Response
{
$view = new BlogPostView(
$this->blogRepository->findBySlug($slug)
);
return new Response(200, $this->objectRenderer->render($view);
}
}
Now without creating any templates the first thing that will happen is you will get an exception message:
Unable to find template at `templates/BlogPostView.twig`
Behind the scenes the template paths are mapped based on the FQN prefix:
[
'MyApp\\View\\' => 'templates'
'MyApp\\Entity\\' => 'templates/entity'
]
Exception Driven Development ¶
So then, let’s create a template:
<html>
<head>
<title>{{ view.title }}</title>
</head>
<body>
{{ render(view.post) }}
</body>
</html>
We call render(<object>)
to render the Post
object and we will get an
exception:
Unable to find template at `templates/entity/Post.twig`
Great! Let’s create that:
<article>
<h2>{{ view.title }}</h2>
<div>
{{ view.body }}
</div>
{% for tag in view.tags %}{{ render(tag) }}{% endfor %}
</article>
Note that the template always has exactly one parameter: view
. Can you guess
what happens next?
Unable to find template at `templates/entity/Tag.twig`
And so on. I like this - it tells you what it needs and is easy to reason about - you have an object? render it. This creates very strong boundaries around templates - preventing them from getting to greedy.
Usually when I implement this pattern I think - oh! I need to pass extra parameters to the template:
$blogPost = $repository->find('my-blog-post');
$renderer->render($blogPost, [
'next' => 'https://www....',
'previous' => 'https://www....',
]);
But no! This is not needed! We need actually to create a new object containing those parameters:
$renderer->render(
new BlogPostView(
$blogPost,
new Link('next'),
new Link('previous')
)
);
This is awesome in a subtle way - we can put all that behavior which may have ended up in the template into an object and we can benefit from all the nice things that implies like testing and static analysis.
The Root of All Evil ¶
But the best thing is inheritence (hear me out, it’s might be fine).
Let’s say we Post
extends Article
extends Content
. The object renderer
will automatically fall back to the parent class if the top class does not
exist.
Because templates map exactly to a certain class FQN, they are guaranteed to work with any objects extending that class.
So for example, if we are rendering a list:
$list = new List([
new Header([
new StringCell('ID'),
new StringCell('Title'),
new StringCell('Last Modified'),
new StringCell('Action'),
]),
new ListBody([
new ListRow(
new IntegerCell(5),
new StringCell('Foobar'),
new DateCell(new DateTime('05-05-2020')),
new LinkCell('Edit', 'https://example.com/edit/this')
),
// ...
])
);
echo $renderer->render($list);
Imagine StringCell
and IntegerCell
nodes extend ScalarCell
, then we
would need only to create ScalarCell
to render both. Later when we add
FloatCell
it wil just work, while providing us a way to specialize.
Also, by running our application we would effectively be prompted to create each successive template (when we stick to the rule of calling the renderer for all objects).
Into The Wild ¶
I’ve used this so far on phpactor to render markdown help for code reflection elements (e.g. rendering method / class / type information).