Is there any reason to use Twig with WordPress? Do you like PHP code snippets inside your HTML layout templates, can you open any large template and find a part you are interested in within a few seconds? If you can, you probably have a small template file.

When I first started using CMSes written in PHP, I was unpleasantly surprised by the way they produce HTML code. I tried one CMS and then another, however found the same situation, HTML and PHP code snippets were tightly coupled and mixed in a chaotic way. If you have a template file containing less then 200 lines of code it may seem not so scary, but in case of large projects you may end up spending hours trying to found unclosed tag, bracket or quote. Furthermore, WordPress has introduced a really bad practice – opening a tag in one file and close it in another one.

PHP Echo

Are PHP and HTML best friends?

I have noticed that sometimes newbie developers don’t understand how does the HTML/CSS/JS frontend trio relate to “server side languages” like PHP. Why do I put the server side word into quotes? Because, to be honest there is only one thing that makes a language to be a server side, is ability to handle requests coming from the client-side. On the low level it means that a language has built-in features for opening sockets and handling requests of a specific protocol, in our case HTTP. As you may already be aware about Node JS, this a good example of a language that initially was used only inside a browsers’ virtual machine. We have a standard specification called ECMAScript. Now it i implemented on the server side and used quite heavily for different kind of applications. NodeJS has a great asynchronous I/O and request processing.

Why I am telling you this? In order help you grasp an important idea. There are a lot of different languages that can be used for producing HTML code, they all have different syntax, features and anytime you mix frontend markup languages your are cornering yourself into boundaries, since you will have to change a lot of code if the backend technology changes. To avoid such problems you have to keep your templates (layout) files decoupled from your server side technologies. Template engines usually use some sort of DSL that is processed by a template engine itself. In future articles I will show how can you create a web site (powered for example by WordPress) from HTML layout without making a single change in HTML files.

Why use a template engine?

Extracting HTML from PHP

  • Decoupling – As I have already mentioned, decoupling is a great way to make your applications more scalable and extendable, since you are keeping different concerns separetely.
  • Speed and Efficiency – Development becomes much faster and efficient when a template engine is used, I know you are already tired of openning and closing PHP tags inside your templates for just a single echo statement. Of course compilation of templates requires extra processing time, but it’s worth it.
  • Convention – A template engine force you to use some convention and you usually don’t have a choice to implement something quite differently.
  • Readability and Simplicity – Typically, a template engine’s syntax is much more readable than code snippets. That saves a lot of time while development and debuggings.
  • Security – While writing echo snippets or similar constructions you have more freedom, I have seen a number of time, when devlopers put everything inside a template file starting from core business rules (like calculating salary) to raw SQL queries. When you are using a template engine you at least will have to find how to implement your dirty idea, but I hope you won’t find and end up with moving all non related thigns to a proper layer. In addition some template engines provide security features like sandboxing.

Meet Twig

Twig is a template engine for the PHP programming language that is built to boost your development experience and save a lot of time and spare your nerves. It will bring new advantages in your development process and help to eliminate spaghetti code. Twig is becoming even more popular and widely used, for instance, Twig is the core template engine in Drupal 8. Opencart starting from 3.0.0 version is also leverages the Twig template engine. I guess that WordPress starting from 5.0.0 also will use Twig or some other template engine.

Lets have a look at some sort of before-after example.

Without using Twig.

1
2
3
4
5
6
7
8
9
   <div class="recent-posts">
        <h2 class="recent-posts__title">Recent Posts</h2>
        <ul class="recent-posts__list">
            <?php foreach ($posts as $id => $post) { ?>
                <li class="recent-posts__list-item recent-post-item <?php echo(($id == $currentPostId) ? 'recent-post-item--current' : ''); ?>"
                    id="<?php echo $id; ?>"><?php echo $post['title']; ?></li>
            <?php } ?>
        </ul>
    </div>

With using Twig.

1
2
3
4
5
6
7
8
<div class="recent-posts">
<h2 class="recent-posts__title">Recent Posts</h2>
<ul class="recent-posts__list">
    {% for id, post in posts %}
        <li class="recent-posts__list-item recent-post-item {{ (id == currentPostId) ? 'recent-post-item--current': '' }}" id="{{ key }}">{{ post.title }}</li>
    {% endfor %}
</ul>
</div>

This is not the best example, but even so you can see that Twig provides much more readable and concise syntax than PHP echo statements.

Twig is a really powerful and feature-rich template engine, you can find a lot of articles explaining it’s features in details. This article is intended to show how could you use Twig to make your WordPress development experience a little bit better and faster.

Timber (Twig for WordPress)

Twig itself is a template engine for PHP, you may have a question, why do you need another plugin to start using Twig. The answer is – for your convenience. There is a great plugin called Timber. This plugin provides a bunch of useful functions that are closely related to WordPress. For instance, for retrieving posts, pages, filters, custom attributes, etc. You will have a chance to look at some features in the article or you could refer to the official documentation for more details. So install the Timber plugin and activate it.

WordPress Theming with Twig

Let’s get started. In this tutorial I will use a real project, a new site for our software development company – Incresive. We will use an existing default theme Twenty Seventeen, rewriting it using Twig, also changing some parts and structure of the default theme. I won’t cover a whole process in this article, because it requires a series of articles and doesn’t make big sense.

Directory structure

First copy the default Twenty Seventeen theme and rename directory according to your project name, in my case it will be incresive. Then, create a new directory and call it templates. We will put all twig template files here. There can be a long discussion regarding a directory structure used for a project. Generally, I prefer the package by feature approach, however in that case there are no components/modules/features that have pronounced boundaries. Anyway, we will try to keep our codebase clean and extendable.

Another directory we will have is page-composer. It will contain PHP classes that are responsible for collecting data to be displayed on a page.

1
2
3
4
--<theme_name>
----page-composers
----templates
....

WordPress and OOP

To be honest, WordPress is not a framework/CMS that is written considering OOP principles, it has some classes, however mostly they don’t make sense in the OOP context. There is nothing wrong with that, there are reasons for such decisions, for example backward compatibility, speed and ease of understanding. Following “true” OOP principles may introduce more disadvantages than advantages. All OOP paradigms, principles are usually ovewhelmed by a lot of abstraction, Inversion of Control and boilerplate code. As a result that dramatically complicates the learning curve and requires implementing additional mechanisms, like caching to have the same performance.

Page Composer

A Page Composer is a class (an object of that class) that collects all necessary data for rendering page, after all data is fetched the flow is transferred to Timber, for rendering the page. It is an attempt to even more decouple details from domain and make our theme more OOP driven.

Let’s create an abstract class called Page_Composer_Contract. I am using Contract suffix, it is just mine preference for naming interfaces instead of the traditional I prefix. The file is itself is called interface-page-composer.php, following the “WordPress naming convention”.

1
2
3
4
5
6
7
<?php
interface Page_Composer_Contract
{
    public function render();
    public function get_template();
}
?>

As you can see from the class above, we have two methods. The render method, from the first sight, you can think that a page composer will render a page by itself, but as you will see later it is used only for delegating the flow of control to Timber. The get_template is used to return a template name for a page.

The Template Method pattern

Why do we need to have a separate method instead of directly passing a template twig file name to Timber? Consider the case when you have a page composer class and you just want to change the template with a new one, if you don’t have such method, you will have to override the whole render method copying and pasting code from the parent class. However, if you have a specific method for returning the name of a template file, we have to just override a single method. This pattern is called Template Method Pattern.

Now let’s define a class that will be used as the base class for all page composers, that will contain common logic. In that case I will show only one case – adding specific classes to the body tag according to a device used by a user. The file name is abs-class-base-page-composer.php, I don’t know whether this naming conforms to WordPress naming conventions, but let it be like that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
require_once 'interface-page-composer.php';
require_once get_template_directory() . '/utils/Mobile_Detect.php';

abstract class Base_Page_Composer implements Page_Composer_Contract
{
    const CLASS_IOS = 'mobile-device--ios';
    const CLASS_ANDROID = 'mobile-device--android';
    const CLASS_MOBILE = 'mobile-device';
    protected $context;
    protected $device_detector;

    public function init()
    {
        $this->context = Timber::get_context();
        add_filter('body_class', array($this, 'detect_device_class'));
    }

    public function detect_device_class($classes)
    {
        $this->device_detector = new Mobile_Detect();
        $body_class_name = '';
        if ($this->device_detector->isMobile()) {
            $classes[] .= self::CLASS_MOBILE;
            if ($this->device_detector->is('iOS')) {
                $classes[] = self::CLASS_IOS;
            }
            if ($this->device_detector->is('Android')) {
                $classes[] = self::CLASS_ANDROID;
            }
        }
        $classes[] = $body_class_name;
        $classes[] = 'body-main';
        return $classes;
    }

    public function collect_body_styles()
    {
        // Collect classes for body tag
        $body_classes = get_body_class();
        $this->context['body_properties'] = '';
        if (in_array(self::CLASS_IOS, $body_classes)) {
            $this->context['body_properties'] .= 'ontouchstart=""';
        }
        $this->context['body_classes'] = join(' ', $body_classes);
    }
}

?>

I think that the code above is clear. The context property is a variable that stores all data that is passed to a template by Twig (Timber). We will discuss it later in that article.

The Theme class

Before starting to discover templates, I want to introduce one more idea in order to improve codebase of your WordPress theme. If you have previous WordPress development background, you are certainly aware of the functions.php file. Usually this is the largest file in a WordPress theme. Newbie and even experienced developers are tend to put almost everything here. This makes this file a huge ball of mud, containing unrelated logic and concerns. Let’s improve this situation a little bit.

The main idea behind that class is to have a single place where all theme related logic is stored and implemented. This class could be classified as the God object antipattern, but in that case time spent trying to improve this, normally doesn’t worth it.

I am sure you have heard about the Singleton pattern, also it is referred as an antipattern very often. However, considering the nature of the PHP language and generally a wordpress theme is a single unit, as a result we will make the class to implement that pattern.

I will show you a real class that is used by my personal website. You should get a basic idea.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
<?php
class Crosp_Theme
{
    private static $instance;
    public static $image_sizes = [
        'crsp-image-xxlarge' => ['width' => 2048, 'height' => 0, 'crop' => false],
        'crsp-image-xlarge' => ['width' => 1920, 'height' => 0, 'crop' => false],
        'crsp-image-large' => ['width' => 1600, 'height' => 0, 'crop' => false],
        'crsp-image-slarge' => ['width' => 1200, 'height' => 0, 'crop' => false],
        'crsp-image-xmedium' => ['width' => 1024, 'height' => 0, 'crop' => false],
        'crsp-image-medium' => ['width' => 800, 'height' => 0, 'crop' => false],
        'crsp-image-small' => ['width' => 640, 'height' => 0, 'crop' => false],
        'crsp-image-xsmall' => ['width' => 480, 'height' => 0, 'crop' => false],
        'crsp-image-tiny' => ['width' => 320, 'height' => 0, 'crop' => false],
        'crsp-image-xtiny' => ['width' => 160, 'height' => 0, 'crop' => false],
    ];
    private $timber;

    const ACTION_LOAD = 'load';
    const ACTION_DATA = 'data';
    private $actions = [
        'main_sidebar' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'admin_hooks' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'timber_engine' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'custom_theme_support' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'menu_custom_fields' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'general_setup' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'enqueue_scripts' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'enqueue_styles' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
        'custom_thumbnail_sizes' => [self::ACTION_LOAD => false, self::ACTION_DATA => array()],
    ];
    private function __construct()
    {

    }
    public static function get_instance()
    {
        if (null === static::$instance) {
            static::$instance = new Incresive_Theme();
        }

        return static::$instance;
    }

    public function add_custom_theme_support_features()
    {
        add_theme_support('post-thumbnails', array('post', 'page'));
        add_theme_support('automatic-feed-links');
        add_theme_support('title-tag');
        add_theme_support('html5', array(
            'search-form', 'comment-form', 'comment-list', 'gallery', 'caption',
        ));
        add_theme_support('post-formats', array(
            'aside', 'image', 'video', 'quote', 'link',
        ));
    }

    public function register_main_sidebar()
    {
        register_sidebar(array(
            'name' => __('Sidebar Main'),
            'id' => 'sidebar-main',
            'description' => 'Main blog sidebar',
            'before_widget' => '<aside id="%1$s" class="widget %2$s">',
            'after_widget' => '</aside>',
            'before_title' => '<h3 class="widget-title"><span class="widget-title__text">',
            'after_title' => '</span></h3>',
        ));
    }

    public function setup_timber_engine()
    {
        // Init Timber and Twig template language
        Timber::$locations = array(FileStoreManager::theme_directory('/templates'),
            FileStoreManager::theme_directory('/templates/post'),
            FileStoreManager::theme_directory('/templates/header'),
            FileStoreManager::theme_directory('/templates/footer'),
            FileStoreManager::theme_directory('/templates/navigation')

        );
        $this->timber = new TimberTimber();
    }

    public function enqueue_scripts()
    {
        wp_scripts()->add_data('jquery', 'group', 1);
        wp_scripts()->add_data('jquery-core', 'group', 1);
        wp_scripts()->add_data('jquery-migrate', 'group', 1);

        wp_enqueue_script('main', FileStoreManager::theme_directory_uri('/js/main.js'), array('jquery'), NULL, true);
    }

    public function enqueue_styles()
    {
        wp_register_style('bootstrap-css', FileStoreManager::theme_directory_uri('/css/blog/dependencies/bootstrap.min.css'));
        wp_enqueue_style('bootstrap-css');
        wp_enqueue_style('body-fonts', 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700');
        wp_enqueue_style('common-style', get_stylesheet_uri());
    }

    public function add_custom_menu_fields_support($custom_fields = array())
    {
        Custom_Menu_Fields::init($custom_fields);
    }

    public function set_admin_hooks()
    {
        add_filter('tiny_mce_before_init', array($this, 'change_mce_options'),1);
    }

    public function change_mce_options($init)
    {
        $init["forced_root_block"] = "h4";
        $init["force_br_newlines"] = true;
        $init["force_p_newlines"] = false;
        $init['valid_children'] = "+a[div|p|ul|ol|li|h1|span|h2|h3|h4|h5|h5|h6]";
        $init["convert_newlines_to_brs"] = true;
        return $init;
    }

    public function custom_settings_fields()
    {
        // Blog description
        register_setting('general', 'blog_short_description', 'esc_attr');
        add_settings_field('blog_short_description',
            '<label for="blog_short_description">' . __('Blog description', 'crosp-blog') . '</label>', array(&$this, 'settings_input_field'), 'general', 'default', array('name' => 'blog_short_description'));
        // Title for main page
        register_setting('general', 'site_main_title', 'esc_attr');
        add_settings_field('site_main_title',
            '<label for="site_main_title">' . __('Site Main Title', 'crosp-blog') . '</label>', array(&$this, 'settings_input_field'), 'general', 'default', array('name' => 'site_main_title'));
    }

    function settings_input_field($args)
    {
        $value = get_option($args['name'], '');
        echo '<input type="text" id="' . $args['name'] . '" name="' . $args['name'] . '" value="' . $value . '" />';
    }

    public function general_setup()
    {
        add_filter('admin_init', array(&$this, 'custom_settings_fields'));
        add_filter('the_generator', array($this, 'remove_wordpress_version_meta'));
        remove_filter( 'the_content', 'wpautop' );

        $this->setup_plugin_filter();
    }

    public function remove_wordpress_version_meta()
    {
        return '';
    }

    public function setup_plugin_filter()
    {
        add_filter(Accordion_Categories_Widget::FILTER_CATEGORY_ICON, array(&$this, 'provide_category_icon'), 10, 2);
    }

    public function provide_category_icon($category_icon, $category_id)
    {
        return get_option(sprintf(CATEGORY_ICON_OPTION_PATTERN, $category_id));
    }

    public function register_custom_thumbnail_sizes($sizes = array())
    {
        add_theme_support('post-thumbnails');
        if (!empty($sizes)) {
            foreach ($sizes as $key => $value) {
                add_image_size($key, $value['width'], $value['height'], $value['crop']);
            }
        }
    }


    public function get_attachment_url_by_slug($slug)
    {
        $args = array(
            'post_type' => 'attachment',
            'name' => sanitize_title($slug),
            'posts_per_page' => 1,
            'post_status' => 'inherit',
        );
        $_header = get_posts($args);
        $header = $_header ? array_pop($_header) : null;
        return $header ? wp_get_attachment_url($header->ID) : '';
    }

    // Builder chain methods
    public function with_main_sidebar()
    {
        $this->actions['main_sidebar'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_timber_engine()
    {
        $this->actions['timber_engine'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_scripts()
    {
        $this->actions['enqueue_scripts'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_admin_hooks()
    {
        $this->actions['admin_hooks'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_general_setup()
    {
        $this->actions['general_setup'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_custom_theme_support()
    {
        $this->actions['custom_theme_support'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_styles()
    {
        $this->actions['enqueue_styles'][self::ACTION_LOAD] = true;
        return $this;
    }

    public function with_custom_thumbnail_sizes($sizes = array())
    {
        $this->actions['custom_thumbnail_sizes'][self::ACTION_LOAD] = true;
        $this->actions['custom_thumbnail_sizes'][self::ACTION_DATA] = $sizes;
        return $this;
    }

    public function with_menu_custom_fields($custom_fields = array())
    {
        $this->actions['menu_custom_fields'][self::ACTION_LOAD] = true;
        $this->actions['menu_custom_fields'][self::ACTION_DATA] = $custom_fields;
        return $this;
    }
    // Proceed actions added with builder
    // Should be called in functions or on early stage of loading
    public function proceed_actions()
    {
        if ($this->actions['timber_engine'][self::ACTION_LOAD]) {
            $this->setup_timber_engine();
        }
        if ($this->actions['general_setup'][self::ACTION_LOAD]) {
            add_action('after_setup_theme', array($this, 'general_setup'));
        }
        if ($this->actions['main_sidebar'][self::ACTION_LOAD]) {
            add_action('widgets_init', array($this, 'register_main_sidebar'));
        }
        if ($this->actions['admin_hooks'][self::ACTION_LOAD]) {
            $this->set_admin_hooks();
        }
        if ($this->actions['enqueue_scripts'][self::ACTION_LOAD]) {
            add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
        }
        if ($this->actions['enqueue_styles'][self::ACTION_LOAD]) {
            add_action('wp_enqueue_scripts', array($this, 'enqueue_styles'));
        }
        if ($this->actions['custom_thumbnail_sizes'][self::ACTION_LOAD]) {
            $this->register_custom_thumbnail_sizes($this->actions['custom_thumbnail_sizes'][self::ACTION_DATA]);
        }
        if ($this->actions['menu_custom_fields'][self::ACTION_LOAD]) {
            $this->add_custom_menu_fields_support($this->actions['menu_custom_fields'][self::ACTION_DATA]);
        }
        if ($this->actions['custom_theme_support'][self::ACTION_LOAD]) {
            $this->add_custom_theme_support_features();
        }
    }
}
?>

As you can see, this class is de facto a wrapper for the functions.php file. It contains all actions/hooks, enqueues scripts and styles, etc.

You may already have noticed with-* methods. This is an example of the Builder pattern. You can choose depending on different circumstances what hooks to call, scripts to enqueue. The initial idea was to have an array of actions and associated data and finally run just a simple for loop over that array, but I forgot to refactor it. I intentionally left it as it is, try to refactor this code yourself.

How the functions.php was modified? Here is the code from my project, without any changes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
require_once get_template_directory() . '/helpers.php';
global $theme;
if (Utils::is_not_admin_page()) {
    if (Utils::is_front_static_page()) {
        require_once get_template_directory() . '/class-main-cv-theme.php';
        $theme = Main_CV_Theme::get_instance();
        $theme->
        with_timber_engine()->
        with_general_setup()->
        with_styles()->
        with_scripts()->
        with_custom_theme_support()->
        with_custom_thumbnail_sizes(Main_CV_Theme::$image_sizes)->
        proceed_actions();

    } // If any other blog page
    else {
        require_once get_template_directory() . '/class-main-theme.php';
        // Theme instance
        $theme = Main_Theme::get_instance();
        $custom_menu_fields = [
            'menu-icon' => 'Menu font icon'
        ];
        // Setup theme for blog pages
        $theme->
        with_timber_engine()->
        with_general_setup()->
        with_styles()->
        with_scripts()->
        with_main_sidebar()->
        with_menu_custom_fields($custom_menu_fields)->
        with_custom_theme_support()->
        with_custom_thumbnail_sizes(Main_Theme::$image_sizes)->
        proceed_actions();
    }
} else {
    // Init theme parts required by admin side
    require_once get_template_directory() . '/class-main-cv-theme.php';
    require_once get_template_directory() . '/class-main-theme.php';
    $theme_main = Main_Theme::get_instance();
    $custom_menu_fields = [
        'menu-icon' => 'Menu font icon'
    ];
    $theme_main->
    with_general_setup()->
    with_main_sidebar()->
    with_admin_hooks()->
    with_menu_custom_fields($custom_menu_fields)->
    with_custom_theme_support()->
    with_custom_thumbnail_sizes(Main_CV_Theme::$image_sizes)->
    proceed_actions();

    // Init themes for admin
    $theme_cv = Main_CV_Theme::get_instance();
    $theme_cv->with_general_setup()->
    with_custom_theme_support()->
    with_custom_thumbnail_sizes(Main_CV_Theme::$image_sizes)->
    proceed_actions();
}
?>

We have eliminated all complexity from the functions.php and left a few lines of code. Furthermore, I have separated theme classes, because my blog pages and the front page look quite differently. However, as I always worry, do not treat my code snippets as the best way to follow, clean code or whatever. Even in that small code snippet there are a bunch of things that have to be refactored.

Twig Templates

Finally, everything is ready to start creating Twig templates for our theme. There is a starer theme provided by developers of Timber. I suggest you to have a look at it in order to get understanding of what you are able to do using the plugin and Twig itself.

Base template

Initial step is to create a base template file that will contain common parts for any page. Let’s create a file called base.twig and define it as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends "base.twig" %}

{% block header %}
    <header id="header-main" class="l-header header">
        {% include ['front-header.twig'] %}
    </header>
{% endblock %}
{% block main_content %}
    <main id="main-page-content" class="container main-content">
        {% block section_about_us %}
            <section id="about-us" class="section section--white about-us">
                {% include ['section-about-us.twig'] %}
            </section>
        {% endblock %}
        {% block section_latest_blog_posts %}
            <section id="latest-blog-posts" class="section section--dark latest-blog-posts">
                {% include ['section-latest-blog-posts.twig'] %}
            </section>
        {% endblock %}
        {% block section_contacts %}
            <section id="contacts" class="section section--white contacts">
                {% include ['section-contacts.twig'] %}
            </section>
        {% endblock %}
    </main>
{% endblock %}
{% block footer %}
    <footer id="footer-main" class="footer front-footer">
        {% include ['front-footer.twig'] %}
    </footer>
{% endblock %}
{% block out_body %}
    {% include ['scroll-to-top.twig'] %}
{% endblock %}

We have just defined a skeleton for our pages. I think it is quite clear what is going on here, at least the code is readable. The content of included files are not really important in that case as they will be overridden. Let’s discuss the most important parts.

  • Block – blocks represent logical parts of your page, blocks are used for overriding and inheritance. A block is a part of a page that is changing. By analogy they can be considered as methods(not static and private) with echo statements inside. We will see in a while how to override blocks.
  • Include – is a self-explanatory statement that includes content of the specified file, of course all statements in the file being included are processed as well.
  • {{ echo }} – two curly braces are used for printing out content of the variable placed inside.

Front Page Template and Composer

Now it is time to define a template for front page. There is no magic behind Timber (Twig), WordPress is still working in the same way as before. So for the front page there is a file called front-page.php. Create it if you don’t have it. Remove all content and insert the following code.

1
2
3
4
5
6
7
8
9
<?php
require_once FileStoreManager::theme_directory('/page-composers/class-front-page-composer.php');
if ( ! class_exists( 'Timber' ) ) {
    echo 'This page cannot be displayed please contact site administrator';
    return;
}
$page_renderer = new Front_Page_Composer();
$page_renderer->render();
?>

As you may already noticed, we need to create a composer class. Let’s do this right now. I will simplify it to show the basic idea and responsibilities. For now, we will have a simple front page that displays several sections:

  • Header Intro – containing just a name of the site.
  • About Us – content of a page created through the admin panel.
  • Latest Blog Posts – recent blog articles.
  • Contacts – a contact form and addresses.

Now we need to create a template file for the front page and extend from the base template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{% extends "base.twig" %}

{% block header %}
    <header id="header-main" class="l-header header">
        {% include ['front-header.twig'] %}
    </header>
{% endblock %}
{% block main_content %}
    <main id="main-page-content" class="container main-content">
        {% block section_about_us %}
            <section id="about-us" class="section section--white about-us">
                {% include ['section-about-us.twig'] %}
            </section>
        {% endblock %}
        {% block section_latest_blog_posts %}
            <section id="latest-blog-posts" class="section section--dark latest-blog-posts">
                {% include ['section-latest-blog-posts.twig'] %}
            </section>
        {% endblock %}
        {% block section_contacts %}
            <section id="contacts" class="section section--white contacts">
                {% include ['section-contacts.twig'] %}
            </section>
        {% endblock %}
    </main>
{% endblock %}
{% block footer %}
    <footer id="footer-main" class="footer front-footer">
        {% include ['front-footer.twig'] %}
    </footer>
{% endblock %}
{% block out_body %}
    {% include ['scroll-to-top.twig'] %}
{% endblock %}

Header section

For the sake of simplicity, the header section will contain only the title and description of our site. Let’s define the following layout.

1
2
3
4
5
6
<div class="header-intro">
    <div class="header-intro__wrapper">
        <h1 class="header-intro__title header-title">{{ header.title }}</h1>
        <h2 class="header-intro__description header-description">{{ header.description }}</h2>
    </div>
</div>

Now, we need collect the information in order to display it in the section. Create a method called, for instance, collect_header_section_data() and in that case getting required information is quite simple as follows.

1
2
3
4
5
6
7
8
9
<?php
 protected function collect_header_section_data()
    {
        $header_data = [];
        $header_data['title'] = get_bloginfo('name');
        $header_data['description'] = get_bloginfo('description', 'display');
        $this->context['header'] = $header_data;
    }
?>

We are just using WordPress functions to get data.

About section

In order to show how you can embed the content of a post into a section, let’s create a new post, in that case it will be called “Section About Us”. Make it private and remove from the search results (sitemap.xml). That may be not the best idea to use a separate post for the section on the front page, however in that case we leave pure WordPress functionality. As a result all plugins that use core WordPress functionality, like Translation plugins, will work fine.

1
2
3
4
5
6
7
<div class="about-us">
    <h2 class="about-us__title">{{ about.title }}</h2>
    <hr class="about-us__divider">
    <p class="about-us__content">
        {{ about.content }}
    </p>
</div>

To get the post I use the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
   protected function collect_about_section_data()
    {
        $about_us_post = Wordpress_Helper::get_private_post_by_slug("section-about-us");
        $about_us_data = [];
        $about_us_post = get_object_vars($about_us_post);
        $about_us_data['title'] = $about_us_post['post_title'];
        $about_us_data['content'] = $about_us_post['post_content'];
        $this->context['about'] = $about_us_data;
    }
?>

Latest blog posts section

Next section will contain three recent posts. Here is a simple layout for that section.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="blog-posts blog-posts--latest row">
    {% for post in latest_posts %}
        <div class="blog-post__item blog-post-item col-md-4">
            <img alt="{{ post.title }}" src="{{ post.thumbnail.src('crsp-image-small') }}"
                 class="blog-post-item__bg-image">
            <div class="blog-post-item__info post-item-info">
                <a class="post-item-info__categories">
                    {% for category in post.terms('category') %}
                        <a class="post-item-info__category-link"
                           href="{{ category.link }}">{{ category.name }}{{ not loop.last ? ',' : '' }}</a>
                    {% endfor %}
                </a>
                <h3 class="post-item-info__title">
                    <a class="ost-item-info__title-link" href="{{ post.link }}" rel="">{{ post.title }}</a>
                </h3>
                <span class="post-item-info__date">
                    {{ post.date }}
                </span>
            </div>
        </div>
    {% endfor %}
</div>

For that section we have used the for loop for iterating through latest posts. Also we have the nested loop through categories. Now let’s see how can we get latest posts in our composer class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
    public function before_collect()
    {
        add_filter('timber_post_getter_get_posts', array($this, 'post_additional_info'), 10, 3);
    }

    public function post_additional_info($posts)
    {
        foreach ($posts as $post_object) {
            $post_object->date_link = Wordpress_Helper::get_post_date_link($post_object->post_date);
            $post_object->views_count = Wordpress_Helper::get_post_view_count($post_object->id);
        }
        return $posts;
    }

    protected function collect_latest_blog_posts_data()
    {
        $args = array(
            'posts_per_page' => self::POSTS_COUNT,
            'status' => 'publish',
            'ignore_sticky_posts' => 0
        );
        $posts = Timber::get_posts($args);
        $this->context['latest_posts'] = $posts;
    }
?>

As I have already mentioned Timber provides a better integration between WordPress and Twig. Timber library provides a number of useful functions. In that case we are calling the Timber::get_posts() method. That is just a wrapper method that adds some additional conditions and filters. In the code above we are adding a filter handling function that just adds additional info to each fetched post. This filter is triggered by Timber.

Contacts section

Finally, the last section is Contacts. For that section we will use the well known plugin Contact Form 7. Create a form in the admin section and you will get a shortcode code for the just created form. I’ve got the following value [contact-form-7 id=”18″ title=”Contact Front Page”]. The template file for that section is simplified and has the next content.

1
2
3
4
5
6
7
8
<div class="contacts">
    <h3 class="contacts__title">
        {{ contacts.title }}
    </h3>
    <div class="contacts__form contact-form contact-form--front">
        {{ contacts.form }}
    </div>
</div>

And to get the contact form, we are using the do_shortcode method as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
    protected function collect_contacts_section_data()
    {
        $form_html = do_shortcode(self::CONTACT_FORM_FRONT_PAGE_SHORTCODE);
        $contacts_data = [];
        $contacts_data['title'] = __('Get in touch');
        $contacts_data['form'] = $form_html;
        $this->context['contacts'] = $contacts_data;
    }
?>

And finally as we’ve reached the last section and have all collect* method defined we need to create/override the render method, that is actually passing all collected data to Timber.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
    public function render()
    {
        $this->init();
        $this->before_collect();
        $this->collect_header_section_data();
        $this->collect_about_section_data();
        $this->collect_latest_blog_posts_data();
        $this->collect_contacts_section_data();
        Timber::render($this->get_template(), $this->context);
    }

    public function get_template()
    {
        return array('front-page.twig');
    }
?>

Conclusion

In this article I have tried to explain and show how you can be a more productive WordPress developer, eliminate chaotic template files, the functions.php file, etc. Using Timber/Twig with WordPress makes it easy to develop, read and extend even a simple wordpress theme. In further articles I will show you how to create WordPress themes without even modifying a single line of a template file created by a frontend developer.

The features explained in this article are only a minor part of the Timber/Twig functionality. Almost every feature that you need for development is already implemented and supported by the template engine. So do not hesitate to learn more about it.

All in all, main ideas are always the same. Keep your layers separated, use abstraction instead of a concrete implementation and keep codebase clean.

Hi, my name is Molochko Alexander, I am Interested in different areas of software development, curious about learning and discussing architectural and software patterns, examining internals and understanding how everything works under the hood.