Inpsyder Guido writes about WordPress and Twig. He focuses on how to separate Data from Views to have a cleaner and more testable code.
When having the first approach to WordPress, every developer has started to create templates either for plugins or themes. And every developer knows how difficult it can be to maintain a template that mixes up HTML and PHP code when more features are added to that template. The file gets more complex. And the more things you add and the more code you touch the more difficult does it get fix issues.
Yes, you can split the templates in several parts. And each one will have its own concerns. But most of the time you expose data from the parent to the child template. And that’s not good.
Another thing developers usually do is to use the PHP extract functions to create variables from data collections (usually array structures). That’s not only an uncontrolled way. Moreover, the function takes a secondary parameter flag by default set to EXTR_OVERWRITE and usually, developers don’t change it. So other variables with the same name get overwritten. And that’s not good.
Besides those problems, we as developers like to have a clean and well-formatted code. Code that is: easy to read, self-explanatory, easy to maintain – and even more important: code that is testable.
Mostly, code gets difficult to be tested when everything is within a template. That’s because your test has to be aware of WordPress. You have to mock a lot of functions. When you want to test your code, you also want to test the logic of your code and not WordPress. And this is a bit difficult to achieve if everything is inside a template.
We’ll not discuss the unit test in this article but if you want to get deeper into unit testing without WordPress you are served well by this Unit Test for Php code without WordPress article part1 and part2
How to solve those problems?
If you ever heard of MVC you’ll probably have an idea how the before mentioned problems can be solved.
Model–view–controller is an architectural pattern commonly used for developing user interfaces that divides an application into three interconnected parts. This is done to separate internal representations of information from the ways information is presented to and accepted from the user.
Even though implementing a real MVC pattern sometimes can be considered over-engineering, the logic of that pattern could be the base for our approach. In a very abstracted way, we could consider WordPress as our Controller and divide the View from the Model where the view is the markup and the model is the data we want to inject into the markup. When we split the logic of the model from the view, we can produce a more testable and well maintainable code that is also less error-prone.
But how do we split model from view?
Here is where twig and data structures come into play.
Twig
Twig is a template engine. A template engine is a software that combines data and view to produce a document. In our case an HTML document or part of it.
Data Structures
In the simplest way, we could define data structures as a collection of values. Php is shipped with a very basic data structure named array. But PHP 7 expands that by including more specific data structures as an alternative of the array. For more information, you can read the Efficient data structures for php 7. In our implementation, we’ll stay on arrays, because all of the PHP versions support them and they are easy to manage.
Combine Twig and Data Structures
Let’s say we want to display the thumbnail image within a single blog post. To do so we hook into the the_content filter and make use of twig we’ll obtain the parsed markup then we prepend the image to the post content. For our implementation we’ll use twig-wordpress, a simple library to add support for WordPress filters and functions to twig. We’ll also use some other libraries to define the WordPress model and the view. Then we’ll put everything together.
Twig for WordPress
When we use twig for WordPress we can create a service that helps us rendering our templates. The simplest way to create that service is by using the Twig for WordPress Factory like so:
$twig = new Factory(new FilesystemLoader(__DIR__ . '/views/'), []);
$twig = $twig->create();
$twigController = new TwigController($twig);
Taking a closer look we see that the code creates a new TwigEnvironment instance (TwigEnvironment will render our template) using the FilesystemLoader (the loader of the template). The path passed to the FilesystemLoader constructor is the base path where the templates are located. We assume we are looking for templates under the main root plugin directory and we are executing the code above from a script that is also under the main root.
Twig has different kinds of loaders. You can load the template from files, from arrays or chaining loaders (loaders that will be executed in a chain to find the template to load).
So, now we have the twig environment that is responsible of render our template. What we need next is the model and the view. Moreover, we need a way to pass those into the twig env to obtain our template markup output.
Model and View
The model here is the most interesting part. As developers, we’d like to encapsulate our code. So we’ll build a class that implements the WordPressModel\Model interface that has only one method data responsible of return the data structure we want to inject in our template.
interface Model
{
/**
* Data
*
* @return array The list of elements to bind in the view
*/
public function data(): array;
}
The exact implementation can be viewed into the PostThumbnail class. This will then be injected into the TwigData, a class that will contain the model and the path where the view is located.
final class PostThumbnail implements Model
{
const FILTER_DATA = 'twigwordpresslightexample.attachment_image';
const FILTER_ALT = 'twigwordpresslightexample.attachment_image_alt';
/**
* @var
*/
private $attachmentSize;
/**
* @var int
*/
private $attachmentId;
/**
* @param int $attachmentId
* @param mixed $attachmentSize
*/
public function __construct(
int $attachmentId,
$attachmentSize = 'thumbnail'
) {
$this->attachmentId = $attachmentId;
$this->attachmentSize = $attachmentSize;
}
/**
* @return array
*/
public function data(): array
{
$imageSource = $this->attachmentSource();
/**
* Figure Image Data
*
* @param array $data The data arguments for the template.
*/
return apply_filters(self::FILTER_DATA, [
'image' => [
'attributes' => [
'url' => $imageSource->src,
'class' => 'thumbnail',
'alt' => $this->alt(),
'width' => $imageSource->width,
'height' => $imageSource->height,
],
],
]);
}
/**
* @return \stdClass
*
* @throws \InvalidArgumentException If the attachment isn't an image.
*/
private function attachmentSource(): \stdClass
{
if (!wp_attachment_is_image($this->attachmentId)) {
throw new \InvalidArgumentException(
'Attachment must be an image.'
);
}
$imageSource = wp_get_attachment_image_src(
$this->attachmentId,
$this->attachmentSize
);
if (!$imageSource) {
return (object)[
'src' => '',
'width' => '',
'height' => '',
'icon' => false,
];
}
$imageSource = array_combine(
['src', 'width', 'height', 'icon'],
$imageSource
);
return (object)$imageSource;
}
/**
* @return string
*/
private function alt(): string
{
$alt = get_post_meta(
$this->attachmentId,
'_wp_attachment_image_alt',
True
);
/**
* Filter Alt Attribute Value
*
* @param string $alt The alternative text.
* @param int $attachmentId The id of the attachment from which
* the alt text is retrieved.
*/
$alt = apply_filters(self::FILTER_ALT, $alt, $this->attachmentId);
return (string)$alt;
}
}
What the class basically does is building the data for the view. And as you can see in the code below there is no black magic or difficult things to understand. We’re simply encapsulating some WordPress logic under private methods to better maintain them. And it returns the data needed for the view.
return apply_filters(self::FILTER_DATA, [
'image' => [
'attributes' => [
'url' => $imageSource->src,
'class' => 'thumbnail',
'alt' => $this->alt(),
'width' => $imageSource->width,
'height' => $imageSource->height,
],
],
]);
We also pass the data to a filter in case third party developers want to make changes to it.
Now that we have our model, we need a view. Creating a view is simple. As we defined that our twig loader is a filesystem loader, we simply create a new .twig file under the views directory named for example thumbnail.twig that will contain the markup and the property accessors to our model.
{% if image.attributes.url %}
<img src="{{ image.attributes.url }}"
class="{{ image.attributes.class }}"
alt="{{ image.attributes.alt }}"
width="{{ image.attributes.width }}"
height="{{ image.attributes.height }}"
/>
{% endif %}
Twig uses the double curly braces to identify which kind of data has to be replaced with values.
So the {{ image.attributes.url }} will be interpolated to the value contained into $imageSource->src. The {{ image.attributes.class }} to thumbnail and so on.
At the end our markup will look like this:
<img
src="http://dev.local/wp-content/uploads/2018/10/thumbnail.jpg"
class="thumbnail"
alt="Alternative text for the image"
width="800"
height="800"
/>
Twig also allows you to use control structures. This is useful to check against values so the expression {% if image.attributes.url %} means that the markup will be displayed only if the URL contains a non-empty value.
Build Things Together
Now that we have the Model and the View we want to render them by using the twig environment instance. To do so, we can use the classes from the package Twig WordPress View. The package consists of two classes, a Controller and a ViewData. The first one is used to inject the model into the right view by using the twig env instance. And the second one is a data class that contains the instance of the model and the view file path.
So, the first thing we do is to create an instance of ViewData and an instance of the Controller. Then we pass the instance of ViewData into the Controller::render method. Internally the Controller::render method will use the twig env to load the view file. Then it will call TwigEnvironment::display by passing the data from the model.
$model = new Model\PostThumbnail($postThumbnailId, 'post-thumbnail');
$viewData = new TwigData($model, 'thumbnail.twig');
Then we want to pass it into our controller to be rendered:
$twigController->render($viewData);
Let show everything together.
add_filter(
'the_content',
function ($content) use ($twigController) {
if (!is_singular()) {
return $content;
}
ob_start();
$postThumbnailId = (int)get_post_thumbnail_id();
if ($postThumbnailId < 1) {
return $content;
}
$model = new Model\PostThumbnail( $postThumbnailId, 'post-thumbnail' );
$viewData = new TwigData($model, 'thumbnail.twig');
$twigController->render($viewData);
return ob_get_clean() . $content;
}
);
A note of the use of ob_start() and ob_get_clean() methods. Since the render method will try to output the markup, we have to get the content of that output from the output buffer so we can concatenate it as a string to the content of the post. And that is, we can reuse the $twigController to print other twig data by simply create a new model class and a view.
I created a TwigData class instead of passing the model and the template path directly into the render method to encapsulate them making more simple to move within the code when for example you want to create the data for twig somewhere else. I created a repo where it is possible to play a bit with the code. In case you get interested, you can clone the repo at https://github.com/widoz/twig-wordpress-light-example.
Conclusion
There are a lot more things to discuss when we get into template engines, but I hope that with this article you get a different point of view on how templates can be done in WordPress. Also, there are many ways how it is possible to split the concerns for models and views. This is only one of them, not necessarily the best one, but it is one.
If you want to read more about twig, I would suggest you have a look at: sandboxes, partials, and cache. You can read more about those under the Advanced Twig section.
Failed to submit:
one or more fields are invalid.