Last time, we explored the benefits of (1) unconditionally setting variables with the default filter and (2) requiring fields. We'll wrap up with three more ways to remove control flow from your code.

3. Route your content

Let's say we are templating a structure with multiple entry types: neighborhood, city, and state.

In our _entry template, we might create a switch statement to include sub templates that are unique to the entry type.

{# _entry #}
...
{% switch entry.type %}
    {% case 'neighborhood' %}
        {% include "_includes/neighborhood" %}
    {% case 'city' %}
        {% include "_includes/city" %}
    {% case 'state' %}
        {% include "_includes/state" %}
{% endswitch %}
...

Not bad. We are still keeping our code DRY by including only the content that is different.

The flaw here is subtle – notice that we are telling our template how to identify the correct template based on the entry type.

If we add another type called country, we suddenly have two areas to maintain: the new country include and our switch statement.

Can we take the control flow—the how—out of the equation and just tell the template what we want?

Let's make two assumptions about these entry types:

  1. Their names cannot be influenced by the user
  2. They always have an associated include

Both are realistic expectations. Now, we can dynamically declare the correct include by concatenating the entry type to the template lookup.

{# _entry #}
...
{% include "_includes/" ~ entry.type %}
...

We just turned this template into a router.

This technique is a simple application of polymorphism—providing the same interface for different types. Pretty slick, huh?

Let's see how well this approach works for a Craft matrix with three block types: plainText, image, and button. Here's the switch way...

{% for block in matrix %}
    {% switch block.type %}
        {% case 'plainText' %}
            <p>{{ block.plainText }}</p>
        {% case 'image' %}
            <figure>
                <img src="{{ block.img.first().getUrl() }}">
                {% if block.caption %}
                    <figcaption>{{ block.caption }}</figcaption>
                {% endif %}
            </figure>
        {% case 'button' %}
            <a href="{{ block.websiteUrl }}" class="btn btn--large">
                {{ block.text }}
            </a>
    {% endswitch %}
{% endfor %}

…and the declarative way:

{% for block in matrix %}
    {% include "_includes/" ~ block.type %}
{% endfor %}

Which is better in this case? It depends on your matrix. The plain text include would look like this:

<p>{{ block.plainText }}</p>

Feels a bit lonely, right? Even our image sub template is a little sad:

<figure>
    <img src="{{ block.img.first().getUrl() }}">
    {% if block.caption %}
        <figcaption>{{ block.caption }}</figcaption>
    {% endif %}
</figure>

So is the lowly switch better?

Let's say we have more than one type of matrix in Craft (we'll call it anotherMatrix), but they share some of their block types. What if we stored all of our block type templates in a folder called _blocks to be accessible by both?

Now routing really shows off its colors.

TL;DR: The more complex your data, the more benefits you get from routing instead of switching.

4. Block sections that may change

Blocks are to sections what the default filter is to variables: forgiving.

If you find yourself rewriting a section of _layout in only one or two sub templates, a block is the perfect solution to declare an easily overridable default.

Marketing sites often share common sections between templates, like a subscribe form or a testimony carousel. Let's block these below our content block (not inside—Twig will crash if you block within another block):

{# _layout #}
{% block content %}
{% endblock %}
{% block global %}
    {% include "_includes/subscribe" %}
    {% include "_includes/_testimony" %}
{% endblock %}

We may not want these default sections to show up in the blog index. No problem!

{# blog/index #}
{% extends "_layout" %}
{% block content %}
    ...
{% endblock %}
{% block global %}
    {# empties block #}
{% endblock %}

We can also add to the block with {{ parent() }}:

{% block global %}
    {{ parent() }}
    {% include "_includes/archive" %}
{% endblock %}

Blocks are also great for liquid sections like scripts, meta data, and the hero section.

TL;DR: If a section in your _layout changes frequently but needs sane defaults, use a block to DRY your code but allow sub template customization.

5. Relocate your code

Often, we can remove the question mark from our templating by simply migrating code up or down the tree structure to a template where we know certain variables exists. Here's the polite way:

{# _layout #}
{% if craft.request.getSegment(2) == 'blog' %}
    {% set headline = global.fullName ~ "'s Blog" %}
{% elseif entry is defined and entry.section == 'calendar' %}
    {% set headline = 'Event: ' ~ [entry.startDate, entry.endDate] | join(' - ') %}
{% elseif entry is defined %}
    {% set headline = entry.title %}
{% else %}
    {% set headline = siteName %}
{% endif %}
<header>
    <h1>{{ headline }}</h1>
</header>

And here's the declarative way.

{# _layout #}
{% set headline = headline | default(entry.title | default(siteName)) %}
<header>
    <h1>{{ headline }}</h1>
</header>
{# blog/index #}
{% set headline = global.fullName ~ "'s Blog" %}
{# calendar/_entry #}
{% set headline = 'Event: ' ~ [entry.startDate, entry.endDate] | join(' - ') %}

TL;DL: Before pulling out the if statement, ask yourself if the information is defined in a parent or child template.

Conclusion

You will never need to use an if statement after reading this article, right?

There are still many situations where "iffing" is the most idiomatic solution, but if you find yourself conditionally creating variables and nesting your if statements, there may be a more sustainable solution.

TL;DR: When you are unsure of your content, declare before you ask. Add dependable fallbacks with Twig's default filter and require the fields you absolutely need. Route your data where you can, block sections for easy overriding, and move your code to where your variable call is not a question.

I'm a green developer (it's my 1 year anniversary!), so if you've discovered other ways to simplify code and defeat non-existent content, comment below!