Using Twig* and Symfony2 forms in WordPress

* good bye loop, my old friend

@codekipple

 

Carl Hughes

WordPress Nirvana

  • Template engine
  • Framework for forms
  • Thumbnailing on the fly
  • Help with routing issues
  • Custom fields framework
    that matches ACF features
  • Editor, drop in content modules
    (a pipe dream, for now)

Searching for...

What Twig?

  • Template engine for php
  • Built by Sensio Labs (Symfony2)
  • Stand alone component
  • Can and is being used in many different frameworks/cms's

"Because WordPress is awesome, but the loop isn't."
- http://upstatement.com/timber/

Why Twig?

  • Side step the loop!
  • Separate logic from presentation
  • Reuse view templates/patterns (OOCSS)
  • Share view templates with living styleguide
  • Works brilliantly with child themes

"The most fun I have writing html" - Me

How Twig?

Install Timber https://wordpress.org/plugins/timber-library/

 

Maintained by Jared Novack, under active development. Active on github and open to pull requests.

 

Specific docs for Timber - https://github.com/jarednova/timber/wiki

 

Twig docs - http://twig.sensiolabs.org/documentation

The old way

<?php get_header(); ?>

<main class="site-main" role="main">

    <?php if ( have_posts() ) : ?>

        <?php if ( is_home() && ! is_front_page() ) : ?>
            <header>
                <h1 class="page-title screen-reader-text"><?php single_post_title(); ?></h1>
            </header>
        <?php endif; ?>

        <?php
        // Start the loop.
        while ( have_posts() ) : the_post();
            get_template_part( 'content', get_post_format() );
        endwhile;

    // If no content, include the "No posts found" template.
    else :
        get_template_part( 'content', 'none' );
    endif;
    ?>

</main><!-- .site-main -->

<?php get_footer(); ?>

index.php

The new way

<?php
$context = Timber::get_context();
$context['posts'] = Timber::get_posts();
$templates = array('index.twig');
Timber::render($templates, $context);

index.php

{% extends "base.twig" %}

{% block content %}
    {% for post in posts %}
	{% include ['tease-'~post.post_type~'.twig', 'tease.twig'] %}
    {% endfor %}
{% endblock %}

index view file extends the base view file

<!doctype html>
<html>
<head></head>
<body>
    {% block header %}
        <a href="{{ site.link }}" class="site-logo" aria-label="homepage" title="homepage">Twig!</a>
    {% endblock %}

    <main role="main" class="page-content">
        {% block content %}
            {{ post.content }}
        {% endblock %}
    </main>

    {% block footer %}
        <footer class="page-foot">© copyright{{ "now"|date('Y') }}</footer>
    {% endblock %}
</body>
</html>

Template Inheritance ... blocks

base.twig

Build a base "skeleton" template that contains all the common elements of your site and defines blocks that child templates can override.

{% extends "base.twig" %}

{% block content %}
    {% for post in posts %}
	{% include ['tease-'~post.post_type~'.twig', 'tease.twig'] %}
    {% endfor %}
{% endblock %}

index view file extends the base view file

Reusable patterns

<div class="tease {% if image %}has-img{% endif %}">
    <h2 class="tease__title"><a href="{{ link }}">{{ title }}</a></h2>
    {% if image %}
        <a href="{{ link }}"><img class="tease__img" src="{{ image.src }}" alt="{{ image.alt }}" /></a>
    {% endif %}
    {{ body_text }}
    <a href="{{ link }}" class="btn btn--tertiary">Read more</a>
</div>
{% for item in events %}
    {% include "partials/tease.twig" with {
        'image': item.image,
        'title': item.title,
        'body_text': item.get_preview(80),
        'link': item.link
    } only %}
{% endfor %}

partials/tease.twig

{% for item in news %}
    {% include "partials/tease.twig" with {
        'title': item.title,
        'body_text': item.get_preview(40),
        'link': item.link
    } only %}
{% endfor %}

WP Integration

{{ function('wp_footer') }}

Including PHP or WordPress functions

Timber::get_posts($query = false, $class = 'TimberPost');

Timber:get_posts (returns TimberPost)

much, much more ...

Caching, TimberMenu, TimberPost, TimberSite, TimberTerm, TimberUser, Timber Filters ...

TimberPost: Used to represent posts retrieved from WordPress, making them available to Twig templates.

<article>
    <h1 class="headline">{{post.post_title}}</h1>
    <div class="body">
        {{post.content}}
    </div>
</article>

Image resizing

<img src="{{post.thumbnail.src|resize(300, 200)}}" />

All of these filters are written specifically to interact with WordPress's image API. (So don't worry, no weird TimThumb stuff going on -- this is all using WP's internal image sizing stuff).

Integration with Jetpacks Photon

Image resizing on the fly, no longer want to use the regenerate thumbnails plugin.

Forms

"Forms are hard" - Me

"Dealing with HTML forms is one of the most common - and challenging - tasks for a web developer." - Symfony2 site

  • WordPress offers little help
  • Plugins often rely on admin configuration

Symfony2 Forms

  • Well tested, maintained
  • Request handling
  • Validation
  • CRSF protection
  • Easy Api
  • Twig integration
  • Configured in code rather than admin
    (gravity forms and similar plugins)
  • Let me display the form and it's fields in any
    bizarre way I want.
<?php
$formFactory = WiredForms\Form_Factory::Instance()->formFactory;
$form_builder = $formFactory->createBuilder()
    ->add('message', 'textarea', array(
        'constraints' => new NotBlank(array(
            'message' => 'This value should not be blank.'
        ))
    ))
    ->add('email', 'text', array(
        'constraints' => new NotBlank(),
        'constraints' => new Email(array(
            'message' => 'The email {{ value }} is not a valid email.',
        ))
    ));

$form = $form_builder->getForm();
$request = Request::createFromGlobals();
$form->handleRequest($request);

if ($form->isValid()) {
    $data = $form->getData();
    // perform some action, such as saving the task to the database
}

$form = $form_builder->getForm();
Timber::render(array('contact.twig'), array('form' => $form->createView()));

Creating a form

Displaying the form

To display the form in Twig you have to use Twig Bridge, it provides.

  • Form related Twig functions
  • A base form template
{# Widgets #}

{% block form_widget -%}
    {% if compound %}
        {{- block('form_widget_compound') -}}
    {% else %}
        {{- block('form_widget_simple') -}}
    {% endif %}
{%- endblock form_widget %}

{% block form_widget_simple -%}
    {% set type = type|default('text') -%}
    <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{%- endblock form_widget_simple %}

{% block form_widget_compound -%}
    <div {{ block('widget_container_attributes') }}>
        {%- if form.parent is empty -%}
            {{ form_errors(form) }}
        {%- endif -%}
        {{- block('form_rows') -}}
        {{- form_rest(form) -}}
    </div>
{%- endblock form_widget_compound %}

{% block collection_widget -%}
    {% if prototype is defined %}
        {%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%}
    {% endif %}
    {{- block('form_widget') -}}
{%- endblock collection_widget %}

{% block textarea_widget -%}
    <textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
{%- endblock textarea_widget %}

{% block choice_widget -%}
    {% if expanded %}
        {{- block('choice_widget_expanded') -}}
    {% else %}
        {{- block('choice_widget_collapsed') -}}
    {% endif %}
{%- endblock choice_widget %}

{% block choice_widget_expanded -%}
    <div {{ block('widget_container_attributes') }}>
    {%- for child in form %}
        {{- form_widget(child) -}}
        {{- form_label(child) -}}
    {% endfor -%}
    </div>
{% endblock choice_widget_expanded %}

{% block choice_widget_collapsed -%}
    {% if required and empty_value is none and not empty_value_in_choices and not multiple -%}
        {% set required = false %}
    {%- endif -%}
    <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
        {% if empty_value is not none -%}
            <option value=""{% if required and value is empty %} selected="selected"{% endif %}>{{ empty_value|trans({}, translation_domain) }}</option>
        {%- endif %}
        {%- if preferred_choices|length > 0 -%}
            {% set options = preferred_choices %}
            {{- block('choice_widget_options') -}}
            {% if choices|length > 0 and separator is not none -%}
                <option disabled="disabled">{{ separator }}</option>
            {%- endif %}
        {%- endif -%}
        {% set options = choices -%}
        {{- block('choice_widget_options') -}}
    </select>
{%- endblock choice_widget_collapsed %}

{% block choice_widget_options -%}
    {% for group_label, choice in options %}
        {%- if choice is iterable -%}
            <optgroup label="{{ group_label|trans({}, translation_domain) }}">
                {% set options = choice %}
                {{- block('choice_widget_options') -}}
            </optgroup>
        {%- else -%}
            <option value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
        {%- endif -%}
    {% endfor %}
{%- endblock choice_widget_options %}

{% block checkbox_widget -%}
    <input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock checkbox_widget %}

{% block radio_widget -%}
    <input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{%- endblock radio_widget %}

{% block datetime_widget -%}
    {% if widget == 'single_text' %}
        {{- block('form_widget_simple') -}}
    {% else %}
        <div {{ block('widget_container_attributes') }}>
            {{- form_errors(form.date) -}}
            {{- form_errors(form.time) -}}
            {{- form_widget(form.date) -}}
            {{- form_widget(form.time) -}}
        </div>
    {% endif %}
{%- endblock datetime_widget %}

{% block date_widget -%}
    {% if widget == 'single_text' %}
        {{- block('form_widget_simple') -}}
    {% else -%}
        <div {{ block('widget_container_attributes') }}>
            {{- date_pattern|replace({
                '{{ year }}':  form_widget(form.year),
                '{{ month }}': form_widget(form.month),
                '{{ day }}':   form_widget(form.day),
            })|raw -}}
        </div>
    {%- endif %}
{%- endblock date_widget %}

{% block time_widget -%}
    {% if widget == 'single_text' %}
        {{- block('form_widget_simple') -}}
    {% else -%}
        {% set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} %}
        <div {{ block('widget_container_attributes') }}>
            {{ form_widget(form.hour, vars) }}{% if with_minutes %}:{{ form_widget(form.minute, vars) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second, vars) }}{% endif %}
        </div>
    {%- endif %}
{%- endblock time_widget %}

{% block number_widget -%}
    {# type="number" doesn't work with floats #}
    {% set type = type|default('text') %}
    {{- block('form_widget_simple') -}}
{%- endblock number_widget %}

{% block integer_widget -%}
    {% set type = type|default('number') %}
    {{- block('form_widget_simple') -}}
{%- endblock integer_widget %}

{% block money_widget -%}
    {{ money_pattern|replace({ '{{ widget }}': block('form_widget_simple') })|raw }}
{%- endblock money_widget %}

{% block url_widget -%}
    {% set type = type|default('url') %}
    {{- block('form_widget_simple') -}}
{%- endblock url_widget %}

{% block search_widget -%}
    {% set type = type|default('search') %}
    {{- block('form_widget_simple') -}}
{%- endblock search_widget %}

{% block percent_widget -%}
    {% set type = type|default('text') %}
    {{- block('form_widget_simple') -}} %
{%- endblock percent_widget %}

{% block password_widget -%}
    {% set type = type|default('password') %}
    {{ block('form_widget_simple') }}
{%- endblock password_widget %}

{% block hidden_widget -%}
    {% set type = type|default('hidden') %}
    {{- block('form_widget_simple') -}}
{%- endblock hidden_widget -%}

{% block email_widget -%}
    {% set type = type|default('email') %}
    {{- block('form_widget_simple') -}}
{%- endblock email_widget %}

{% block button_widget -%}
    {% if label is empty -%}
        {% set label = name|humanize %}
    {%- endif -%}
    <button type="{{ type|default('button') }}" {{ block('button_attributes') }}>{{ label|trans({}, translation_domain) }}</button>
{%- endblock button_widget %}

{% block submit_widget -%}
    {% set type = type|default('submit') %}
    {{- block('button_widget') -}}
{%- endblock submit_widget %}

{% block reset_widget -%}
    {% set type = type|default('reset') %}
    {{- block('button_widget') -}}
{%- endblock reset_widget %}

{# Labels #}

{% block form_label -%}
    {% if label is not sameas(false) -%}
        {% if not compound -%}
            {% set label_attr = label_attr|merge({'for': id}) %}
        {%- endif %}
        {% if required -%}
            {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
        {%- endif %}
        {% if label is empty -%}
            {% set label = name|humanize %}
        {%- endif -%}
        <label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ label|trans({}, translation_domain) }}</label>
    {%- endif %}
{%- endblock form_label %}

{% block button_label -%}{%- endblock %}

{# Rows #}

{% block repeated_row -%}
    {#
    No need to render the errors here, as all errors are mapped
    to the first child (see RepeatedTypeValidatorExtension).
    #}
    {{- block('form_rows') -}}
{%- endblock repeated_row %}

{% block form_row -%}
    <div>
        {{- form_label(form) -}}
        {{- form_errors(form) -}}
        {{- form_widget(form) -}}
    </div>
{%- endblock form_row %}

{% block button_row -%}
    <div>
        {{- form_widget(form) -}}
    </div>
{%- endblock button_row %}

{% block hidden_row -%}
    {{ form_widget(form) }}
{%- endblock hidden_row %}

{# Misc #}

{% block form -%}
    {{ form_start(form) }}
        {{- form_widget(form) -}}
    {{ form_end(form) }}
{%- endblock form %}

{% block form_start -%}
    {% set method = method|upper %}
    {%- if method in ["GET", "POST"] -%}
        {% set form_method = method %}
    {%- else -%}
        {% set form_method = "POST" %}
    {%- endif -%}
    <form name="{{ form.vars.name }}" method="{{ form_method|lower }}" action="{{ action }}"{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}{% if multipart %} enctype="multipart/form-data"{% endif %}>
    {%- if form_method != method -%}
        <input type="hidden" name="_method" value="{{ method }}" />
    {%- endif -%}
{%- endblock form_start %}

{% block form_end -%}
    {% if not render_rest is defined or render_rest %}
        {{- form_rest(form) -}}
    {% endif -%}
    </form>
{%- endblock form_end %}

{% block form_enctype -%}
    {% if multipart %}enctype="multipart/form-data"{% endif %}
{%- endblock form_enctype %}

{% block form_errors -%}
    {% if errors|length > 0 -%}
    <ul>
        {%- for error in errors -%}
            <li>{{ error.message }}</li>
        {%- endfor -%}
    </ul>
    {%- endif %}
{%- endblock form_errors %}

{% block form_rest -%}
    {% for child in form -%}
        {% if not child.rendered %}
            {{- form_row(child) -}}
        {% endif %}
    {%- endfor %}
{% endblock form_rest %}

{# Support #}

{% block form_rows -%}
    {% for child in form %}
        {{- form_row(child) -}}
    {% endfor %}
{%- endblock form_rows %}

{% block widget_attributes -%}
    id="{{ id }}" name="{{ full_name }}"
    {%- if read_only %} readonly="readonly"{% endif -%}
    {%- if disabled %} disabled="disabled"{% endif -%}
    {%- if required %} required="required"{% endif -%}
    {%- for attrname, attrvalue in attr -%}
        {{- " " -}}
        {%- if attrname in ['placeholder', 'title'] -%}
            {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}"
        {%- elseif attrvalue is sameas(true) -%}
            {{- attrname }}="{{ attrname }}"
        {%- elseif attrvalue is not sameas(false) -%}
            {{- attrname }}="{{ attrvalue }}"
        {%- endif -%}
    {%- endfor -%}
{%- endblock widget_attributes %}

{% block widget_container_attributes -%}
    {%- if id is not empty %}id="{{ id }}"{% endif -%}
    {%- for attrname, attrvalue in attr -%}
        {{- " " -}}
        {%- if attrname in ['placeholder', 'title'] -%}
            {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}"
        {%- elseif attrvalue is sameas(true) -%}
            {{- attrname }}="{{ attrname }}"
        {%- elseif attrvalue is not sameas(false) -%}
            {{- attrname }}="{{ attrvalue }}"
        {%- endif -%}
    {%- endfor -%}
{%- endblock widget_container_attributes %}

{% block button_attributes -%}
    id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif -%}
    {%- for attrname, attrvalue in attr -%}
        {{- " " -}}
        {%- if attrname in ['placeholder', 'title'] -%}
            {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}"
        {%- elseif attrvalue is sameas(true) -%}
            {{- attrname }}="{{ attrname }}"
        {%- elseif attrvalue is not sameas(false) -%}
            {{- attrname }}="{{ attrvalue }}"
        {%- endif -%}
    {%- endfor -%}
{%- endblock button_attributes %}

base form template provided by Twig Bridge

Displaying the form ... Continued

<form action="{{ post.link }}" class="contact-form" method="post" {{ form_enctype(form) }}>
    {{ form_row(form.message, {
        'label_attr': {'class': 'form__label'},
        'attr': {'placeholder': 'Type your message here'}
    }) }}

    {{ form_row(form.name, {
        'label_attr': {'class': 'form__label'},
        'attr': {'placeholder': 'name'}
    }) }}

    {{ form_row(form.email, {
        'label_attr': {'class': 'form__label'},
        'attr': {'placeholder': 'email'}
    }) }}

    {{ form_widget(form) }}
</form>

How to integrate forms

"require": {
    "symfony/form": "2.5.3",
    "symfony/validator": "~2.2",
    "symfony/http-foundation": "~2.2",
    "symfony/security-csrf": "2.5.3",
    "symfony/twig-bridge": "2.5.3",
    "symfony/translation": "2.3.*",
    "symfony/config": "2.3.*",
    "symfony/yaml": "2.5.3"
}

How to integrate forms ... continued

  • Integrate with the HttpFoundation Component
  • Configure CSRF and set a unique token
  • Configure Twig via form Extension and twig bridge.
  • Hook up translations (required for twig default theme) 
  • ​Integrate with Symfony2 Validator component. 
  • Create a form factory class

Not the most straight forward thing to setup.

I'm planning on making a WP plugin which takes care of the implementation for you.

Following along with the configuration guide you will have to.

Example implementation

My implementation can be found here:

https://github.com/codekipple/wp-day-one

Thanks