Programming in Twig

marion.newlevant.com/programming-in-twig-peers-2016

(tl;dr)

{# comments #}

{{ output }}

{% tags %}


{% extends '_layouts/standard' %}
{% import '_macros/product' as m_product %}

{% block content %}
  <section id="portfolio-items">
    {# loop over the featured products #}
    {% for product in g_specials.featuredProducts %}
      <div class="eight columns">
        {{ m_product.thumb(product,
                           {style: 'specials'}) }}
      </div>
    {% endfor %}
  </section>
{% endblock content %}
    

Values & Variables

Twig Values

Simple Values

  • strings:   'single quotes' "double quotes"
  • numbers:   1, 2.5, -7
  • booleans:   true and false
  • null

Sequential Arrays

  • [1, 2, 3]
  • ['foo', 0.5]
  • []
  • ['foo', 'bar', 0, []]


Associative Arrays

  • { key: value, key2: value2 }
  • {
      cat: 'Puff',
      dogs: ['Fido', 'Rover']
    }

Any PHP value

Twig can't create complex php objects, but it can manipulate them.

Common examples: EntryModel ElementCriteriaModel

Inside Sequential arrays

foo[0] — the first element in foo

foo[1] — the second element in foo



Inside Associative arrays

foo['bar'] — the value associated with the key bar

dereferencing

foo.bar

  1. Check if foo is an associative array with key bar
  2. — same as foo['bar']
  3. Check if foo is object with property bar
  4. Check if foo is object with method bar
  5. Check if foo is object with method getBar
  6. Check if foo is object with method isBar
  7. Give up and throw an error

Creating Variables

The "set" tag


{% set foo = ['Fido', 'Rover'] %}
      

{% set foo %}
...bunch of template code...
{% endset %}
      

Output


{{ value }}
  

Convert value to a string

Print that string out inline


{% set greeting = 'hello, world' %}
{{ greeting }}
  

hello, world
  

Operations (new values from old)

  • operators
  • functions
  • filters

Operators

  • arithmetic: (+, -, *, /, %, etc.)
  • logic: (and, or, not)
  • comparison: (==, !=, <, >, <=, >=)
  • string concatenation: (~)       'foo'~'bar' is 'foobar'
  • string comparisons: (starts with, ends with, matches)
    'foobar' starts with 'foo'

    'foobar' matches '/^f.*r$/'
  • containment: (in)
    1 in [1, 2, 3]

    'foo' in 'foobar'

? Operators

  • ternary: ( ?: )
    (foo ? 'yes' : 'no')
    (foo ?: 'no')(foo ? foo : 'no')
    (foo ? 'yes') (foo ? 'yes' : '')
  • null coalescing operator: ( ?? )
    this ?? that.thing ?? theOther ?? 23

Functions

The usual syntax:   functionName(param, param)

{{ dump(thing) }}   displays all the gory details of thing (assuming you have Craft's devMode turned on.)

Filters

value | filterName

value | filterName(param)

{{ 'hello, world' | upper }}


HELLO, WORLD
      

Who defines these things?

  • twig core
  • Craft
  • plugins

Useful array operations

length

length is a filter which returns the number of items in an array, or the length of a string:


{{ [1, 2, 3] | length }}
    

3
    

{{ 'foobar' | length }}
    

6
    

keys

keys is a filter which returns the keys of an associative array:


{% set pets = {
  cat: 'Puff',
  dog: 'Spot'
} %}

{{ dump(pets | keys) }}
    

array (size=2)
  0 => string 'cat' (length=3)
  1 => string 'dog' (length=3)
    

join

join is a filter which concatinates the elements of an array and returns a string:


{{ pets | keys | join(', ') }}
    

cat, dog
    

{{ pets | join(', ') }}
    

Puff, Spot
    

merge

merge filter merges two arrays (regular or associative)


{% set a = ['Fido', 'Rover'] | merge(['Spot']) %}
    

['Fido', 'Rover', 'Spot']
    

Use it to add something to an existing array


{% set a = a | merge(['Rex']) %}
    

['Fido', 'Rover', 'Spot', 'Rex']
    

merge (associative arrays)

merge operates on keys. If the key doesn't exist, it is added. If it does exist, its value is overridden:


{% set defaultPets = {cat: 'Puff', dog: 'Spot'} %}
{% set myPets = {fish: 'Wanda', cat: 'Fang'} %}

{% set pets = defaultPets | merge(myPets) %}
    

{
  cat: 'Fang',
  dog: 'Spot',
  fish: 'Wanda'
}
    

Conditionals and Loops

  • if
  • for

"if" tag


{% if someValue %}
  ...
{% elseif someOtherValue %}
  ...
{% else %}
  ...
{% endif %}
    

The elseif and else parts are optional.


{% if someValue %} ... {% endif %}
    

Things that are false:

  • false
  • 0
  • ''
  • [] or {}
  • null


Things that are true:

  • everything else including
  • '0'
  • 'false'

"for" tag


{% for foo in arrayLikeThing %}
  ... do something with {{foo}} ...
{% endfor %}
    

foo is only defined inside the for loop.

Array Like Things:

  • arrays:   ['thingOne', 'thingTwo', 3.14159]
  • sequences (syntatic sugar for arrays):   'a'..'z'
  • associative arrays:  { key: value, key2: value2 }
  • any object implementing the Traversable interface.    (e.g. ElementCriteriaModel)

for loop example


{% for animal in ['cat', 'dog', 'fish'] %}
  all about the {{animal}}
{% endfor %}
    

all about the cat
all about the dog
all about the fish
    

Iterating over associative arrays


{% set pets = {
  cat: 'Puff',
  dog: 'Spot'
} %}
    

{% for animal in pets %}
  {{loop.index}}: {{animal}}
{% endfor %}
    

1: Puff
2: Spot
    

Iterating over keys


{% for animal in pets|keys %}
  {{loop.index0}}: {{animal}}
{% endfor %}
    

0: cat
1: dog
    

Iterating over keys and values


{% for species, name in pets %}
  {{species}}: {{name}}
{% endfor %}
    

cat: Puff
dog: Spot
    

'loop'

  • loop.index and loop.index0
  • loop.revindex and loop.revindex0
  • loop.first and loop.last
  • loop.length
  • loop.parent
    (for example: loop.parent.loop.index)

DRY Code

  • macro
  • include
  • extends
  • embed

"macro" tag

define it:


{% macro foo() %}
  ...
{% endmacro %}
    

import it:

defined in the same file


{% import _self as self %}
    

defined in a different file


{% import '_macros/utils' as m_utils %}
    

call it:


{{ m_utils.foo() }}
    

{% set ourFoo = m_utils.foo() %}
    

img macro


{% macro img(asset) %}
  <img src="{{asset.url}}"/>
{% endmacro %}
    

{{ m_utils.img(entry.thumbnail.first) }}
    

{{ m_utils.img(null) }} {# oops! #}
    

{{ m_utils.img() }} {# oops! #}
    

img continued


{% macro img(asset=null) %}
  {% if asset %}
    <img src="{{asset.url}}"/>
  {% endif %}
{% endmacro %}
    

What if we have more information about that asset?


{{ m_utils.img(entry.thumbnail.first,
                    {class: 'thumb'}) }}
    

img continued


{% macro img(asset=null, options={}) %}
  {% set options = {
       class: 'default'
     } | merge(options)
  %}

  {% if asset %}
    <img class="{{options.class}}"
         src="{{asset.url}}"/>
  {% endif %}
{% endmacro %}
    

Recursive Macros


{% macro rNav(pages=[], depth=0) %}
{% import _self as self %}
  <ul data-depth="{{ depth }}">
    {% for page in pages %}
      <li>
        {{ page.link }}
        {% if page.hasDescendents() %}
          {{ self.rNav(page.children, depth+1) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{% endmacro %}
    

{{ m_utils.rNav(craft.entries
                  .section('sitePages').level(1)) }}
    

"include" tag


{% for b in entry.matrixField %}
  {% include '_blocks/text' %}
{% endfor %}
    

Everything that is available in the including template is also available in the included template. b is available.

We can tighten this up:


{% include '_blocks/text'
   with {currentBlock: b} only %}
    

currentBlock will be available... and nothing else

include: smart template names


{% for b in entry.matrixField %}
  {% include '_blocks/'~b.type
     with {currentBlock: b} only %}
{% endfor %}
    

Will include the appropriate template for the block type (assuming you have defined it)


{% include ['_blocks/'~b.type, '_blocks/_default']
   with {currentBlock: b} only %}
  

Will include the template for the block type if it exists, otherwise _blocks/_default.twig

"extends" tag

child.twig


{% extends '_layouts/standard' %}

{% set title = entry.title %}

{% block body %}
  {{entry.body}}
{% endblock body %}
    

Since it extends another template, child.twig is not allowed to produce any output. It can assign values to variables (e.g. title), and define block contents.

_layouts/standard.twig


<title>{{title}}</title>
<body>
  {% block body %}hello, world{% endblock body %}
  {% block foot %}Peers - © 2016{% endblock foot %}
</body>
    

title will be defined here since it was set in child.twig

The body block will be replaced by the body block from child.twig

The foot block will be left untouched.

"embed" tag


...

{% embed '_layouts/mini' with {foo: bar} only %}
  {% block b %}... content...{% endblock b %}
{% endembed %}

...
  
  • Like an include, the contents of the embed will be replaced by _layouts/mini.twig
  • Like an extends, the contents of the b block in mini.twig will be replaced by block content given here.
  • Fine to produce output outside of the embed

When to use: extends, include, macro, embed

  • extends: For layout templates
  • include: When you need the dynamic template names (always use with and only)
  • macro: All other times. Whenever possible.
  • embed: Never (so far)
  • But! do what works for you. Strive for consistancy.

Thank you!

  • marion.newlevant@gmail.com
  • @marionnewlevant
  • http://marion.newlevant.com
  • Marion Newlevant