Le theming avec Drupal 8

A la découverte de

Twig

Présentation

Fabrice Scoyer

Freelance Drupal - Flocon de toile
Logo Flocon de toile

Twig

Twig VS Display Suite / Panels

  • Contrôle total du balisage de vos pages et éléments
  • Réalisation d'agencements / variations complexes difficilement réalisable depuis l'interface d'administration
  • Tout le theming au même endroit
  • Dissocier le front du back facilement

Twig VS PHPTemplate

  • Syntaxe propre plus concise pour manipuler/parcourir des variables
  • Twig est sécurisé par défaut sur Drupal 8 (auto échappement)
  • Twig est extensible : filtres, fonctions
  • Héritage / inclusion de templates permettant de limiter la duplication de code

Template TWIG

Twig vs phptemplate

Débuter avec Twig

  • Activer le debug de Twig : services.yml
  • Le debug natif: dump(var)
  • Un peu mieux avec Kint (module devel) : kint(var)
  • Ou encore xdebug avec Twig xdebug : breakpoint()

Activer le debug de Twig : services.yml

              
  parameters:
    twig.config:
      debug: true
      auto_reload: true
      cache: false
              
            
debug twig actif

Une extension chrome : Drupal template Helper

{{ dump(content) }}

Un peu brut...peut mieux faire

debug avec dump()

{{ kint(content) }}

Mieux mais très looooooonnnnnng

debug avec kint()

{{ breakpoints() }} : top !

debug avec xdebug()

Les templates

Les surcharges de templates


              

Découvertes automatiquement dans le dossier templates du thème

Copier le template ou sa variation dans son thème

Fournir des surcharges de templates

hook_theme_suggestions_HOOK_alter()

              
  function MODULE_theme_suggestions_page_alter(array &$suggestions, array $variables) {
    if ($node = \Drupal::routeMatch()->getParameter('node')) {
      $suggestions[] = 'page__flocon__' . $node->getType();
      $suggestions[] = 'page__flocon__node_' . $node->id();
    }
  }
              
            
suggestions de template

Fournir son propre template

              
  /**
   * Implements hook_theme().
   */
  function MODULE_theme($existing, $type, $theme, $path) {
    return [
      'social_simple_buttons' => [
        'variables' => array(
          'var1' => [],
          'var2' => [],
      ]
    ];
  }
              
            
              
  {# file social-simple-buttons.html.twig #}
  {% if var1 or var2 -%}
    <div class"social-buttons">{{ var1 }} {{ var2 }</div>
  {%- endif %}
              
            

Utiliser son propre template

              
  $build['brocoli'] = [
    '#theme' => 'social_simple_buttons',
    '#var1' => $var1,
    '#var2' => $var2,
  ];
              
            

Utiliser les templates du coeur

exemple avec links.html.twig (voir drupal_common_theme())

              
  /**
   * Implements hook_theme().
   */
  function MODULE_theme($existing, $type, $theme, $path) {
    return [
      'social_simple_buttons' => [
        'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE),
        'preprocess functions' => array('template_preprocess_links'),
      ],
    ];
  }
              
            
              
  {# file social-simple-buttons.html.twig #}
              
            

Utiliser les templates du coeur

avec la surcharge automatique

              
  /**
   * Implements hook_theme().
   */
  function MODULE_theme($existing, $type, $theme, $path) {
    return [
      'links__social_simple_buttons' => [
        'base hook' => 'links',
      ],
    ];
  }
              
            
              
  {# file links--social-simple-buttons.html.twig #}
  {# dans le dossier templates du module ou du thème #}
              
            

Utiliser les templates du coeur

ou directement en construisant le render array

              
  $build['social_simple_buttons'] = [
    '#theme' => 'links__social_simple_buttons',
    '#links' => $links,
    '#attributes' => array(
      'class' => array('links', 'inline', 'social-buttons-links')
    ),
    '#heading' => array(
      'text' => t('Share on '),
      'level' => 'div',
      'attributes' => array(
        'class' => array('social-buttons-title'),
      ),
    ),
  ];
              
            
              
  {# file links--social-simple-buttons.html.twig #}
  {# dans le dossier templates du thème uniquement #}
              
            

Twig et la sécurité

La désinfection des variables est activée automatiquement par Drupal 8

              
  {# Twig sans Drupal 8 : utilisation du filtre e #}
  {{ title|e }}

  {# Twig avec Drupal 8 : le filtre escape est appliqué automatiquement #}
  {{ title }}
              
            

Afficher ses variables dans Twig

Twig autoescape enabled and text sanitization APIs updated

  • Utiliser un template twig intermédiaire à façon (hook_theme)
  • Render array : #markup pour du texte avec html (par défaut Xss::filterAdmin())
  • Render array : #plain_text pour du texte brut (~check_plain)
  • Render array : #item_list pour des listes
  • Render array : #inline_template pour des mini templates twig
  • Retourner des objets FormattableMarkup ou TranslatableMarkup
              
  return ['#plain_text' => $var_safe_or_unsafe ]
  return new TranslatableMarkup($string, $args, $options);
              
            

Avertissement

Twig est sécurisé

PAR DEFAUT

Ce n'est donc pas une vérité absolue.

Avec un peu de bonne volonté on peut très bien en faire un portail d'entrée pour les injections SQL, XSS ou autres joyeusetés

Extension des templates

Etendre un template

Le template de base très très long

              
  {# @file page.html.twig #}
  {% block main %}
    <div role="main" class="main-container {{ container }} js-quickedit-main-content">
    ....
      {# Sidebar Second #}
      {% if page.sidebar_second %}
      {% block sidebar_second %}
      <aside class="col-xs-12 col-sm-4 col-md-3 sidebar sidebar-secondary"">
        {{ page.sidebar_second }}
      </aside>
      {% endblock %}
      {% endif %}
    ...
    </div>
  {% endblock %}
              
            

Etendre un template

La variation qui ne surcharge que le strict nécessaire

              
  {# File page--contact.html.twig #}
  {% extends "page.html.twig" %}
  {% block section %}
    {% set content_classes = [page.sidebar_first or page.sidebar_second ? 'col-xs-12 col-sm-7 col-md-7',] %}
    <section{{ content_attributes.addClass(content_classes) }}>
  {% endblock %}

  {# Sidebar Second #}
  {% block sidebar_second %}
    <aside class="col-xs-12 col-sm-5 col-md-5 sidebar sidebar-secondary" role="complementary">
      {{ page.sidebar_second }}
    </aside>
  {% endblock %}
              
            

Pas de duplication de code,
et on visualise immédiatement la nature de cette variation

Inclure un template

              
  {# @file page.html.twig #}
  {% include 'header.html' %}
  {% block main %}
    <div role="main" class="main-container {{ container }} js-quickedit-main-content">
    ...
    </div>
  {% endblock %}
  {% include 'footer.html' %}
              
            

Décomposer des templates conséquents en plus petits éléments
Améliorer la lisibilité et la maintenabilité

La manipulation des variables : filtres et fonctions

Les filtres Twig Drupal 8 (1/2)

t : traduire une chaine de caractère dans la langue courante

{{ 'Home'|t }}

{% trans %} ... {% endtrans %} : pour une chaine avec arguments

{% trans %}By {{ author_name }}{% endtrans %}

safe_join : pour concaténer un tableau en liste avec séparateur

{{ items|safe_join(', ') }}

without : pour enlever un (ou plus) élément d'un tableau

{{ content|without('field_tags', 'links') }}

Les filtres Twig Drupal 8 (2/2)

clean_class : formater une chaine en une classe HTML valide

{% set classe = view_mode ? 'node--view-mode-' ~ view_mode|clean_class %}

clean_id : formater une chaine en identifiant HTML valide

<div id="{{ custom_var|clean_id }}">

format_date : formater une date

{{ node.createdtime | format_date('publication_date') }}

Les fonctions Twig Drupal 8

url : génère une url absolue depuis une route

{{ url('view.news.page_news') }}

path : génère une url relative depuis une route et un argument

{{ path('entity.node.canonical', {'node': node.id}) }}

link : génère un lien

{{ link(item.title, item.url, { 'class':['btn', 'btn-primary'] }) }}

file_url : génère une url vers un fichier

{{ file_url(node.field_image.entity.uri.value) }}

attach_library : attache une librairie au rendu

{{ attach_library('module/flexslider') }}

Depuis les modules contribués
Twig tweak

drupal_view : embarquer une vue

{{ drupal_view('view_name', 'display_name') }}

drupal_block : embarquer un bloc

{{ drupal_block('news_featured') }}

drupal_entity : embarquer une entité

{{ drupal_entity('node', '3', 'teaser') }}

drupal_token : utiliser les tokens pour récupérer des valeurs

{{ drupal_token('site:name') }}

drupal_config : accéder à la configuration

{{ drupal_config('system.site', 'name') }}

Un mot sur les Attributs


  function MODULE_preprocess_node(&$variables) {
    $attributes = new Attribute(['class' => 'btn']);
    $attributes->addClass('btn-default');
    $variables['var_attributes'] = $attributes;
  }
          
<article{{ var_attributes.addClass(classes) }}>
{{ var_attributes.removeClass('btn-default') }}
{{ var_attributes.setAttribute('id', 'my-id') }}
{{ var_attributes.removeAttribute('id') }}
{% if var_attributes.hasClass('btn-default') %}
<div class"my-class {{ var_attributes.class }}" {{ attributes|without('class') }}>

Travailler avec Twig

ou avec les fonctions de preprocess ?

Fournir ses propres variables

depuis les fonctions de preprocess


  function MODULE_preprocess_node(&$variables) {
    $node = $variables['node'];
    $variables['var1'] = $node->label();
    $variables['var2'] = $var2;
    $variables['var_attributes'] = new Attribute();
  }
          

  {# @file node.html.twig #}
  <div {{var_attributes.addClass('my-class') }}> {{ var1 }} - {{ var2 }}</div>
            

Construire un lien

depuis les fonctions de preprocess


  function MODULE_preprocess_paragraph__paragraph_slide(&$variables) {
    $variables['link'] = [];
    $variables['url'] = '';
    $paragraph = $variables['elements']['#paragraph'];
    $uri = $paragraph->field_paragraph_slide_link->uri;
    $title = $paragraph->field_paragraph_slide_link->title;
    $options = $paragraph->field_paragraph_slide_link->options;
    $options['attributes']['class'][] = 'btn';
    $options['attributes']['class'][] = 'btn-primary';


    if ($title && $uri) {
      $url = Url::fromUri($uri, $options);
      $variables['link'] = Link::fromTextAndUrl($title, $url);
    }

    if ($uri) {
      $variables['url'] = Url::fromUri($uri, $options)->toString();
    }

    $position = new Attribute();
    $class = Html::cleanCssIdentifier($paragraph->field_paragraph_slide_position->value);
    if (empty($class)) {
      $class = 'center';
    }
    $position->addClass($class);
    $variables['position'] = $position;

  }
            

Construire un lien

depuis le template


  {% set link_url = content.field_paragraph_slide_link.0['#url'] %}
  {% set link_title = content.field_paragraph_slide_link.0['#title'] %}
  {# ou une alternative en passant par l'objet et non le render array)
  {% set link_url = paragraph.field_paragraph_slide_link.0.url %}
  {% set link_title = paragraph.field_paragraph_slide_link.0.title %}

  {% if link_url and link_title %}
    <div class="slide-link">
      <a href="{{ link_url }}" class="btn btn-primary">{{ link_title }}</a>
      {# une autre alternative #}
      {{ link(link_title, link_url, { 'class':['btn', 'btn-primary']} ) }}
    </div>
  {% endif %}
            

Privilégier Twig

Pour des ajouts relatifs au theming

  • Gains en performance
  • Maintenance facilitée
La simplicité est la sophistication suprême

Créer ses propres filtres / fonctions

Création d'un service


  # file monmodule.services.yml
  services:
    monmodule.twig_extension:
      class: Drupal\monmodule\TwigExtension
      arguments: ['@entity_type.manager', '@token', '@config.factory', '@current_route_match']
      tags:
        - { name: twig.extension }
            

Créer ses propres filtres / fonctions

Implémentation de Twig_Extension


  /**
   * Twig extension with some useful functions and filters.
   */
  class TwigExtension extends \Twig_Extension {
    public function getFunctions() {
      return [new \Twig_SimpleFunction('my_func', [$this, 'myFunc'])];
    }
    public function getFilters() {
      return [new \Twig_SimpleFilter('uc_words', [$this, 'ucWords'])];
    }
    public function myFunc($arg1, $arg2, $arg3) {
      return some_complex_stuf(...);
    }
    public function ucWords($text) {
      return ucwords($text);
    }
  }
            

Créer ses propres filtres / fonctions

Twig extender permet de créer ses extensions Twig depuis le système de plugin de Drupal 8


  /**
   * Example plugin Hello World.
   *
   * @TwigPlugin(
   *   id = "twig_extender_hello",
   *   label = @Translation("Hello world plugin"),
   *   type = "function",
   *   name = "hello",
   *   function = "hello"
   * )
   */
            

Les bonnes pratiques

Les bonnes pratiques

  • Toujours envoyer aux templates des render array
  • Héritage et inclusion : vous pouvez en abuser
  • On peut manipuler les variables autrement que dans les fonctions de preprocess : depuis le template
  • Les classes dans les templates. Pas dans les preprocess
    • Tout au même endroit
    • Permettre un contrôle total du rendu par une simple surcharge de template

Les bonnes pratiques

  • Twig est sécurisé PAR DEFAUT, mais on peut si on le veut vraiment en faire une passoire
    • SVP, plus de PHP dedans !
    • N'utilisez pas le filtre raw
  • La documentation (variables disponibles, fonction de preprocess)

Merci

Des questions ?