From Template Tags to Twig

A Journey Through WordPress Templating

๐Ÿค“
Follow Along

v.gd/wptemplating

Russell Heimlich
(like the maneuver)

Senior Developer at nclud

Template Tags

<h2>
    <a href="<?php the_permalink() ?>" rel="bookmark">
        <?php the_title(); ?>
    </a>
</h2>
<small>
    <?php the_time(__('F jS, Y', 'kubrick')) ?>
</small>
<div class="entry">
    <?php the_content(); ?>
</div>

the_title() prints the title

get_the_title() returns the value of the title

<header class="entry-header">
    <?php
        if ( 'post' === get_post_type() ) :
            echo '<div class="entry-meta">';
                if ( is_single() ) :
                    twentyseventeen_posted_on();
                else :
                    echo twentyseventeen_time_link();
                    twentyseventeen_edit_link();
                endif;
            echo '</div><!-- .entry-meta -->';
        endif;
        if ( is_single() ) {
            the_title( '<h1 class="entry-title">', '</h1>' );
        } else {
            the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
        }
    ?>
</header><!-- .entry-header -->

Yuck!

Twig

What is Twig?

  • A modern template engine for PHP
  • Different syntax that compiles down to PHP
  • Separates rendering markup from data processing

Twig Syntax

  • Print variables or expressions
    {{ ... }}
  • Evaluate control statements (if, for, while)
    {% ... %}
  • Comments (don't get rendered)
    {# ... #}

An Example

<?php // Loop over some things ?>
<?php foreach ( $things as $thing ) { ?>
  

<?php echo $thing; ?>

<?php } ?>
{# Loop over some things #}
{% for thing in things %}
  

{{ thing }}

{% endfor %}

extends

{% extends "file.twig" %}

{% block content %}
  

This is how you extend a template in Twig

{% endblock %}

Filters


{{ 'fab'|upper }}
    FAB

Passing Data to Templates

<?php
$context = array(
    'foo' => 'bar',
    'baz' => 'bat',
    'url' => get_permalink(),
);
echo $twig->render( 'template.twig', $context );

Why not just use PHP short tags?

<?= $foo ?>
(Don't do this)

Why so much reinventing of PHP?

{% set currentPageNum = paginationCurrentPageNumber %}
{% set totalItems     = paginationTotalItems %}
{% set itemsPerPage   = paginationResultsPerPage %}
{% set pageCount      = (totalItems / itemsPerPage)|round(0, 'ceil') %}
{% set pageRange      = 5 %}

{% if pageCount < currentPageNum %}
    {% set currentPageNum = pageCount %}
{% endif %}

{% if pageRange > pageCount %}
    {% set pageRange = pageCount %}
{% endif %}

{% set delta = (pageRange / 2)|round(0, 'ceil') %}

{% if (currentPageNum - delta) > (pageCount - pageRange) %}

    {% set pages = range((pageCount - pageRange + 1), pageCount) %}

{% else %}

    {% if (currentPageNum - delta) < 0 %}

        {% set delta = currentPageNum %}
    {% endif %}

    {% set offset = currentPageNum - delta %}
    {% set pages  = range(offset + 1, offset + pageRange) %}
{% endif %}

Include Hell

partials/search-tools.twig
partials/stream/stream-home.twig
    partials/stream/featured-posts.twig
        partials/stream/standard/featured.twig
            partials/stream/item-img.twig
            partials/overview/overline.twig
            partials/item-title.twig
            partials/meta-info/meta-info-empty.twig
            partials/meta-info/meta-info-stream.twig
                partials/meta-info/img-' ~ item.get_type ~ '.twig
                partials/meta-info/img.twig
                partials/meta-info/stack.twig
                    partials/meta-info/source-name-' ~ item.get_type ~ '.twig
                    partials/meta-info/source-name.twig
                    partials/meta-info/timestamp-' ~ item.get_type ~ '.twig
                    partials/meta-info/timestamp.twig
                partials/meta-info/source-name-' ~ item.get_type ~ '.twig
                partials/meta-info/source-name.twig

What is going on?!?

Turns out I didn't hate Twig…

I just hated the way it was being used

๐Ÿค”

So how can we use Twig with WordPress?

Timber unites Twig and WordPress

single.php
$context = Timber::get_context();
$context['foo'] = 'Bar!';
$context['post'] = Timber::query_post();
Timber::render( 'single.twig', $context );
single.twig
{% extends "base.twig" %}

{% block content %}
    

{{foo}}

{{post.title}}

{{post.content}}
{% endblock %}

๐Ÿ‘Ž

<a href="{{ post.get_field('custom_link')|e('esc_url') }}"></a>

๐ŸŒฑ Sprig

Bare bones Twig templating support for WordPress

PHP files Lines of Code
Timber 132 18,313
Sprig 2 214

Using Sprig

Sprig::render() will return a string of a compiled template

$context = array(
  'title' => 'Sprig is awesome!',
  'url'   => 'https://github.com/kingkool68/sprig/',
);

// Render the template and return a string
$thing = Sprig::render( 'example.twig', $context );

Sprig::out() will echo a compiled template

$context = array(
  'title' => 'Sprig is awesome!',
  'url'   => 'https://github.com/kingkool68/sprig/',
);

// Echo out the rendered template
Sprig::out( 'example.twig', $context );

Sprig::do_action() will capture the output of a WordPress action and return a string

$context = array(
  'scripts' => Sprig::do_action( 'wp_print_scripts' ),
);

// Echo out the rendered template
Sprig::out( 'example.twig', $context );

Available Twig Filters

More Twig Filters

Add Your Own Twig Filters

function filter_sprig_twig_filters( $filters = array() ) {
    $filters['sanitize_title'] = 'sanitize_title';
    return $filters;
}
add_filter( 'sprig/twig/filters', 'filter_sprig_twig_filters' );

Sprig Filters Example

<input
    type="text"
    name="chart-options[yaxis_label]"
    id="chart-options-yaxis-label"
    value="{{ yaxis_label|esc_attr }}"
>

<a href="{{ link_url|esc_url }}">{{ link_text }}</a>

Available Twig Functions

More Twig Functions

Add Your Own Twig Functions

function filter_sprig_twig_functions( $functions = array() ) {
    $functions['wp_nonce_field'] = 'wp_nonce_field';
    return $functions;
}
add_filter( 'sprig/twig/functions', 'filter_sprig_twig_functions' );

Sprig Functions Example

<select name="chart-options[zoomtype]">
    <option value="x" {{ selected( 'x', zoomtype ) }}>x-Axis Only</option>
    <option value="y" {{ selected( 'y', zoomtype ) }}>y-Axis Only</option>
    <option value="xy" {{ selected( 'xy', zoomtype ) }}>x & y-Axis</option>
    <option value="none" {{ selected( 'none', zoomtype ) }}>None&ly;/option>
</select>
<select name="chart-options[zoomtype]">
    <option value="x" selected="selected">x-Axis Only</option>
    <option value="y">y-Axis Only</option>
    <option value="xy">x & y-Axis</option>
    <option value="none">None&ly;/option>
</select>

Think components
not pages

Data for a component can come from different sources

“You must be shapeless, formless, like water. When you pour water in a cup, it becomes the cup. When you pour water in a bottle, it becomes the bottle. When you pour water in a teapot, it becomes the teapot. Water can drip and it can crash. Become like water my friend.”
—Bruce Lee

Don't tie your components to the data

<?php
/**
 * Handle everything for Archive Items
 */
class RH_Archive_Items {

    /**
     * Get an instance of this class
     */
    public static function get_instance() {
        static $instance = null;
        if ( null === $instance ) {
            $instance = new static();
            $instance->setup_actions();
        }
        return $instance;
    }

    /**
     * Hook into WordPress via actions
     */
    public function setup_actions() { ... }

}

RH_Archive_Items::get_instance();
/**
 * Render an individual archive item
 *
 * @param  array  $args Values to pass to the template to render
 * @return string       HTML of rendered archive item
 */
public static function render_item( $args = array() ) {
    $defaults           = array(
        'url'          => '',
        'title'        => '',
        'excerpt'      => '',
        'date'         => '',
        'display_date' => '',
        'machine_date' => '',
    );
    $context            = wp_parse_args( $args, $defaults );
    $context['title']   = apply_filters( 'the_title', $context['title'] );
    $context['excerpt'] = apply_filters( 'the_content', $context['excerpt'] );

    // More logic for handling data

    return Sprig::render( 'archive-item.twig', $context );
}
<article class="archive-item">
    <h2>
        {% if url %}
            <a href="{{ url|esc_url }}">{{ title }}</a>
        {% else %}
            {{ title }}
        {% endif %}
    </h2>

    {% if display_date %}
        <time datetime="{{ machine_date|esc_attr }}">{{ display_date }}</time>
    {% endif %}

    {{ excerpt }}
</article>
/**
 * Render an archive item from post data
 *
 * @param  WP_Post|integer $post WP Post object or post ID to get data from
 * @param  array           $args Values to override what gets rendered
 * @return string          HTML of rendered archive item
 */
public static function render_item_from_post( $post = null, $args = array() ) {
    $post = get_post( $post );
    $args = array(
        'url'     => get_permalink( $post ),
        'title'   => get_the_title( $post ),
        'excerpt' => get_the_excerpt( $post ),
        'date'    => $post->post_date,
    );
    return static::render_item( $args );
}
/**
 * Render archive items from a WP_Query object
 *
 * @param  object $the_query A WP_Query object
 * @return string            HTML of all archive items
 * @throws Exception         If $the_query isn't a WP_Query object then bail
 */
public static function render_items_from_wp_query( $the_query = false ) {
    global $wp_query;
    if ( ! $the_query ) {
        $the_query = $wp_query;
    }
    if ( ! $the_query instanceof WP_Query ) {
        throw new Exception( '$the_query is not a WP_Query object!' );
    }

    $output = [];
    while ( $the_query->have_posts() ) :
        $post     = $the_query->the_post();
        $output[] = static::render_item_from_post( $post );
    endwhile;
    wp_reset_postdata();
    return implode( "\n", $output );
}
<?php
$context = array(
    'the_loop'   => RH_Archive_Items::render_item_from_wp_query(),
    'pagination' => RH_Pagination::render_from_wp_query(),
);
Sprig::out( 'index.twig', $context );

Living Styleguides

<?php
$basic = RH_Archive_Items::render_item(
    array(
        'title'   => 'Basic Archive Item',
        'excerpt' => 'This is an excerpt to provide more context about what this archive items is all about.',
        'date'    => date( 'Y-m-d' ),
        'url'     => 'https://example.com',
    )
);

$no_date = RH_Archive_Items::render_item(
    array(
        'title'   => 'No Date Archive Item',
        'excerpt' => 'This archive item has no date associated with it.',
        'url'     => 'https://example.com',
    )
);

$no_url = RH_Archive_Items::render_item(
    array(
        'title'   => 'No URL Archive Item',
        'excerpt' => 'You can\'t click the headlines to go somewhere else',
        'date'    => date( 'Y-m-d' ),
    )
);

$context = array(
    'items' => array(
        'Basic'   => $basic,
        'No Date' => $no_date,
        'No URL'  => $no_url,
    ),
);
Sprig::out( 'styleguide-archive-items.twig', $context );

๐Ÿ“–
Resources
&
Further Reading

๐Ÿฅก Takeaways

๐Ÿ”€
Separate data processing from rendering the output

๐Ÿ“„
Use separate files for each template

๐ŸŽŸ
Only pass the data needed to render the template

๐Ÿงถ
Don't constrain templates to their data sources

๐Ÿงฉ
Think in components not pages

Thank You!

Tweet me! @kingkool68

Questions?