Planète

Par DuaelFr
Edouard Cunibil
(ex-)Organisateur de meetups && Co-organisateur du DrupalCamp Montpellier 2014 && Co-organisateur des Drupal Developer Days Montpellier 2015 && Core Contributeur && Module Maintainer && Support sur IRC && Lobbying Twitter && etc. ;)

Installer drush sur un serveur mutualisé OVH.

Drush est devenu un outil incontournable pour le développeur Drupal.

Développer son propre plugin Drupal Crumbs

Le module Drupal Crumbs permet de maitriser son fil d’Ariane (Breadcrumb) selon des critères très complets (views, token, taxonomy, entity reference, etc). Son système de plugins lui permet de supporter toute entité créée sur votre site Drupal. Découvrons comment implémenter notre propre plugin Crumbs pour supporter une entité sur mesure ou encore fournie par un module contribué.

Par AlanT
Alan Tondelier

Les formulaires multi-étapes en ajax avec le module mforms

Les fomulaires multi-étapes permettent de récupérer de nombreuses informations sur l'utilisateur sans l'assomer d'un coup avec une grande quantité d'informations. Pour les besoins d'un projet, je me suis penché sur la réalisation d'un formulaire multi-étapes (ou multi-steps en anglais) sous Drupal.

Je recherchais une solution permettant d'afficher une barre de progression, un nombre variables d'étapes, un comportement en ajax, et la possibilité d'accéder facilement aux données utilisateur saisies. Des solutions envisagées j'ai retenu le module mforms. Le résultat de son implémentation est visible ici.

Retour sur expérience.

Un module complet

Installation & exemple

On commence par récupérer le module.

drush dl mforms -y

Une fois téléchargé, le module mforms est livré avec un module d'exemple complet que je vous recommande d'activer.

drush en mforms,mforms_example -y

Au moment de l'écriture de cet article, il existe un bug dans le module d'exemple concernant la gestion de plusieurs formulaires mforms sur la même page. Si vous rencontrez le soucis, utilisez le module developpement.

Un module, deux approches

Si vous avez activé le module d'exemple, vous pouvez vous rendre sur la page <votredrupal>/mforms pour une page d'exemple de mise en place de votre module.

Le module mforms propose deux façons pour gérer le contenu de vos formulaires multi-étapes :

  1. Un stockage dans la variable $form_state ( variable Drupal de stockage des champs de formulaires )
  2. Un stockage des données dans une varriable de SESSION.

Ces deux approches diffèrent principalement au niveau de la persistance des variables.
En effet les variables stockées avec $form_state seront rafraichies au chargement de la page, l'utilisation de $form_state est à privilégier pour les formulaires multi-étapes "simple" ou la deuxième étapes du fomulaire est une étape de validation de données (par exemple).

À contrario, l'utilisation d'une variable de SESSION va permettre de réutiliser les données saisies par l'utilisateur sur l'ensemble du site Internet mais également de revenir sur les saisies effectuées. C'est cette approche que j'ai retenu pour mon projet.

Construction d'un formulaire multi-étapes

Notre formulaire multi-étapes va faire l'objet d'un module personnalisé. Pour cet exemple je vais appeler le module multi_etapes. Comme nous allons traiter de la réalisation d'un module Drupal, le code sera commenté en anglais pour être en accord avec les bonnes pratiques Drupal. Si certains bouts de codes ne sont pas clairs, n'hésitez pas à poser vos questions en commentaire.

Les bases du module

Je ne reviens pas sur les bases d'un module Drupal, tout est bien expliqué sur la documentation officielle.

multi_etapes.info 

name = Multi Etapes
description = Add a multi-steps session based form. Built with the mforms module.
core = 7.x
; Package name
package = Alan
; Module dependencies
dependencies[] = mforms

Ensuite le fichier .module :

multi_etapes.module

&lt;?php

/**
* @file
* Add a multi-steps session based form. Built with the mforms module.
*/

/**
* Implements hook_permission().
*/
function multi_etapes_permission() {
  return array(
    // 'administer my module' =&gt;  array(
    // 'title' =&gt; t('Administer my module'),
    // 'description' =&gt; t('Perform administration tasks for my module.'),
    // ),
    'access multi-steps forms' =&gt; array(
      'title' =&gt; t('Access multi-steps forms'),
      'description' =&gt; t('Enable the user to fill-in custom multi-steps forms'),
    ),
  );
}

/**
* Implements hook_menu().
*/
function multi_etapes_menu() {

  $items['myform'] = array(
    'title' =&gt; t('My multi-steps form'),
    'page callback' =&gt; 'multi_etapes_myform_page',
    'access arguments' =&gt; array('access multi-steps forms'),
    'type' =&gt; MENU_NORMAL_ITEM,
    'file' =&gt; 'inc/multi_etapes.pages.inc',
  );

  return $items;
}

/**
* Implements hook_STORE_KEY_mforms_init().
*/
function multi_etapes_myform_mforms_init() {
  $store = MformsSessionStore::getInstance('myform');
  $steps = MformsSteps::getInstance($store);

  // If using mustistep controls you need to explicitly define form steps and
  // its attributes.
  $controls = MformsMultiStepControls::getInstance($store, $steps, array(
    '_myform_step1' =&gt; array('value' =&gt; t('Bio'), 'weight' =&gt; -103),
    '_myform_step2' =&gt; array('value' =&gt; t('Hobbies'), 'weight' =&gt; -102),
    '_myform_step3' =&gt; array('value' =&gt; t('Summary'), 'weight' =&gt; -101)
  ));
  // Ajaxify the form stepping process.
  $controls-&gt;ajaxify();

  // Init the mforms.
  mforms_init_module('multi_etapes', 'myform', $store, $controls);
}

Qu'avons-nous fait ?

  1. On créé un droit pour accéder au formulaire multi-étape,
  2. On défini une entrée de menu "classique" pour accéder à ce formulaire (protégée par le droit créé précédemment),
  3. On indique que la fonction de callback "multi_etapes_myform_page" se situe dans le fichier /inc/ du module,
  4. On initialise notre formulaire multi-étapes où myform est la clef via le hook hook_STORE_KEY_mforms_init(),
  5. On défini le nombre d'étapes à l'intérieur du hook et on leur donne un nom (attention au poids),
  6. On fini d'initialiser le formulaire.

inc/multi_etapes.pages.inc

&lt;?php

/**
* Entry page for the multi-step form.
*
* @return array
*   Drupal renderable array.
*/
function multi_etapes_myform_page() {

  // Add user name to steps
  global $user;
  $uname = isset($user-&gt;name)?$user-&gt;name:'Guest';
 
  // Create parameters to be passed to the multi-step form
  $params = array('uname' =&gt; $uname);

  // Return Drupal renderable array.
  return array(
    'mform' =&gt; drupal_get_form('multi_etapes_myform', $params),
  );
}

/**
* Callback function to generate form.
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
* @param array $params
*   Optional params passed into form.
*
* @return array
*   Drupal form array.
*/
function multi_etapes_myform($form, &amp;$form_state, $params) {
  // pass defined parameters to mforms_build
  return mforms_build('myform', '_myform_step1', $form_state, $params);
}

/**
* Callback function to validate form inputs
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
*/
function multi_etapes_myform_validate($form, &amp;$form_state) {
  mforms_validate('myform', $form, $form_state);
}

/**
* Callback function to process the form inputs.
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
*/
function multi_etapes_myform_submit($form, &amp;$form_state) {
  mforms_submit('myform', $form, $form_state);
}

Encore beaucoup d'initialisations dans ce fichier :

  1. On défini la fonction appelée par notre menu,
  2. On récupère le nom de l'utilisateur courant pour montrer le passage de variable à notre formulaire,
  3. On appelle le formulaire via drupal_get_form,
  4. Notre formulaire multi-étapes est défini de façon traditionnelle avec les hook_form , hook_validate et hook_submit.

Il nous reste à mettre en place le code structurant chacune des étapes de notre formulaire.
Le module mform va chercher les étapes ( définies par _myform_stepX() ) dans un fichier qui doit être nommé de la façon suivante : nom_du_module.cleformulaire.inc et placé dans un dossier mforms à la racine de votre module.

Le coeur du fomulaire multi-étapes

Nous allons faire un formulaire d'exemple en 3 étapes :

  1. Informations sur l'utilisateur avec son login récupéré automatiquement, 
  2. Ses activités (Films, livres et sports),
  3. Un résumé des valeurs saisies, un champ pour envoyer un message à l'équipe du site et une case a cocher pour valider et terminer le formulaire.

Ce formulaire reprend les éléments du formulaire d'exemple fourni par le module mforms, en ajoutant des variations qui me semblent intéressantes.

mforms/multi_etapes.myform.inc - Première étape

/**
* First step called by the Mforms state machine.
*
* @param array $form_state
*   Drupal form_state array.
* @param string $next_step
*   Mforms next step callback name.
* @param mixed $params
*   Optional params passed into form.
*
* @return array
*   Drupal form array.
*/
function _myform_step1(&amp;$form_state, &amp;$next_step, $params) {
  // Define following step callback. If none set, that implies it is
  // the last step.
  $next_step = '_myform_step2';
  // Retrieve submitted values. This comes in handy when back action
  // occured and we need to display values that were originaly submitted.
  $data = mforms_get_vals('myform');
  // If we have the data it means we arrived here from back action, so show
  // them in form as default vals.
  if (!empty($data)) {
    $vals = $data;
  }
  elseif (isset($form_state['values'])) {
    $vals = $form_state['values'];
  }
  // Define form array and return it.
  $form = array();
  $form['login'] = array(
    '#type' =&gt; 'textfield',
    '#disabled'=&gt;true,
    '#title' =&gt; t('Login'),
    '#default_value' =&gt; isset($vals['loginv']) ? $vals['loginv'] : $params['uname'], // get login name from previous page (not submited but for design purpose).
  );
  $form['name'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Name'),
    '#default_value' =&gt; isset($vals['name']) ? $vals['name'] : NULL,
  );
  $form['email'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Email'),
    '#default_value' =&gt; isset($vals['email']) ? $vals['email'] : NULL,
    '#required' =&gt; TRUE,
  );
  $form['www'] = array(
    '#type' =&gt; 'textfield',
    '#title' =&gt; t('Your web site'),
    '#default_value' =&gt; isset($vals['www']) ? $vals['www'] : NULL,
  );
  // store the login in a hidden field so that the value is submited
  $form['loginv'] = array(
    '#type' =&gt; 'hidden',
    '#default_value' =&gt; isset($vals['loginv']) ? $vals['loginv'] : $params['uname'],
  );
  return $form;
}
/**
* Validate callback - validates email address.
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
*/
function _myform_step1_validate($form, &amp;$form_state) {
  if (!valid_email_address($form_state['values']['email'])) {
    form_set_error('email', t('Invalid email.'));
  }
}
  1. On défini la fonction de première étape,
  2. On indique l'étape suivante,
  3. On défini les champs à afficher tout en peuplant les valeurs présaisies si elles existent,
    Pour cet exemple, le champ "Login" est récupéré automatiquement. Si l'utilisateur est anonyme, "Guest" est affiché,
  4. On soumet la première étape du fomulaire à la fonction de validation.

mforms/multi_etapes.myform.inc - Deuxième étape

/**
* Step two.
*
* @param array $form_state
*   Drupal form_state array.
* @param string $next_step
*   Mforms next step callback name.
* @param mixed $params
*   Optional params passed into form.
*
* @return array
*   Drupal form array.
*/
function _myform_step2(&amp;$form_state, &amp;$next_step, $params) {
  $next_step = '_myform_step3';
  $form = array();

  $data = mforms_get_vals('myform');

  if (!empty($data)) {
    $vals = $data;
  }
  elseif (isset($form_state['values'])) {
    $vals = $form_state['values'];
  }

  $form['movies'] = array(
    '#type' =&gt; 'textarea',
    '#title' =&gt; t('Movies'),
    '#default_value' =&gt; isset($vals['movies']) ? $vals['movies'] : NULL,
  );
  $form['books'] = array(
    '#type' =&gt; 'textarea',
    '#title' =&gt; t('Books'),
    '#default_value' =&gt; isset($vals['books']) ? $vals['books'] : NULL,
  );
  $form['sports'] = array(
    '#type' =&gt; 'textarea',
    '#title' =&gt; t('Sports'),
    '#default_value' =&gt; isset($vals['sports']) ? $vals['sports'] : NULL,
  );

  return $form;
}

Rien de plus par rapport à l'étape 1, on passe à la dernière étape de ce formulaire.

mforms/multi_etapes.myform.inc - Troisième étape

/**
* Third step.
*
* @param array $form_state
*   Drupal form_state array.
* @param string $next_step
*   Mforms next step callback name.
* @param mixed $params
*   Optional params passed into form.
*
* @return array
*   Drupal form array.
*/
function _myform_step3(&amp;$form_state, &amp;$next_step, $params) {
  $form = array();
  // Get the collected values submited at each step.
  // Here is one difference - the second parameter that defines the step
  // from which we want to retrieve the data.
  $vals1 = mforms_get_vals('myform', '_myform_step1');
  $vals2 = mforms_get_vals('myform', '_myform_step2');
  $form['summary'] = array(
    '#type' =&gt; 'fieldset',
    '#title' =&gt; t('Summary'),
  );
  $form['summary']['sum_bio'] = array(
    '#markup'=&gt;
    "&lt;h3&gt;".t('Bio')."&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;em&gt;".t('Login')." :&lt;/em&gt; ${vals1['loginv']}&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;".t('Name')." :&lt;/em&gt; ${vals1['name']}&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;".t('e-mail')." :&lt;/em&gt; ${vals1['email']}&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;".t('Website')." :&lt;/em&gt; ${vals1['www']}&lt;/li&gt;
     &lt;/ul&gt;",
  );
  $form['summary']['sum_hobbies'] = array(
    '#markup'=&gt;
    "&lt;h3&gt;".t('Hobbies')."&lt;/h3&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;em&gt;".t('Movies')." :&lt;/em&gt; ${vals2['movies']}&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;".t('Books')." :&lt;/em&gt; ${vals2['books']}&lt;/li&gt;
      &lt;li&gt;&lt;em&gt;".t('Sports')." :&lt;/em&gt; ${vals2['sports']}&lt;/li&gt;
     &lt;/ul&gt;",
  );
  $data = mforms_get_vals('myform');
  if (!empty($data)) {
    $vals = $data;
  }
  elseif (isset($form_state['values'])) {
    $vals = $form_state['values'];
  }
  $form['message_team'] = array(
    '#type' =&gt; 'textarea',
    '#title' =&gt; t('A message for the team'),
    '#default_value' =&gt; isset($vals['message_team']) ? $vals['message_team'] : NULL,
  );
  $form['confirm']=array(
    '#type' =&gt; 'checkbox',
    '#title' =&gt; t('Validate and end the survey'),
  );
  return $form;
}
/**
* Validate callback.
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
*/
function _myform_step3_validate($form, &amp;$form_state) {
  if (!$form_state['values']['confirm']) {
    form_set_error('confirm', t('You have to validate the survey in order to continue !'));
  }
}
/**
* Implement submit callback for the last step to process all data submitted.
*
* @param array $form
*   Drupal form array.
* @param array $form_state
*   Drupal form_state array.
*/
function _myform_step3_submit($form, &amp;$form_state) {
  // Here do what you want with the data
   multi_etapes_save_data() // exemple function   // Send a mail to the team if there is a custom message   if($form_state['values']['message_team'] &amp;&amp; !empty($form_state['values']['message_team'])){ // add your own mail function     if(!_multi_etapes_drupal_mail('contact@votresite.com','webmaster@votresite.com',t('Answer to the survey'),$form_state['values']['message_team'])){       drupal_set_message(t('Something went wrong'),'error');     }   }
  // Call mforms_clean();
  // Clear all data from the session variable

  mforms_clean('myform');
  drupal_set_message(t('Thank you for your time.'));
}
  1. Cette fois on affiche un résumé des information saisies précédemment, on utilise la fonction mforms_get_vals avec un second paramètre pour récupérer les valeurs d'une étape en particulier,
  2. On affiche les résultats des deux étapes précédentes,
  3. On affiche un champ pour que l'utilisateur puisse envoyer un message à l'équipe du site,
  4. On met en place une case à cocher pour valider nos résultats et envoyer le message (si saisi),
  5. Une fonction de validation vérifie que la case a bien été cochée lors de l'envoi du formulaire,
  6. La fonction d'envoi traite les données (je ne développe pas ce point, mais on peut imaginer que les données saisies viennent peupler une node ou une entitée personnalisée),
  7. Si l'utilisateur a entré un message à destination de l'équipe on envoi ce message par mail (par exemple),
  8. On vide le contenu de notre formulaire (effacement de la variable de SESSION) et on affiche un message à l'utilisateur.

Et voilà ! Notre module est en place et fonctionnel. Les possibilités sont nombreuses et le système robuste permet de créer n'importe quel type de formulaire multi-étape.

Pour aller plus loin

Quelques astuces complémentaires.

Execution de javascript

Lors de la réalisation du formulaire multi-étape pour le site applicatif de l'eGrid, j'ai eu besoin d'executer du code javascript pour réaliser un carrousel (étape Évaluation).

Le problème avec l'execution de javascript dans les appels en ajax Drupal, c'est qu'il est toujours délicat de savoir à quel moment se "brancher" pour executer son code. Le morceau de javascript suivant permet d'executer le code souhaité à l'étape désirée. Attention cependant le code est réinterprété a chaque reconstruction du DOM (et donc à chaque affichage de message d'erreur de validation).

J'ai placé le code dans js/multi_etapes.js et modifié multi_etapes.info pour déclarer le fichier js ( scripts[] = js/multi_etapes.js ).

(function ($, Drupal, window, document, undefined) {

  Drupal.behaviors.multi_etapes = {
    attach: function(context, settings) {

      if(context === document || context[0].nodeName == 'FORM'){
        var step = $('[id^="edit-steps-btns-wrapper"] .current-step').attr('id');
         
        // si on est bien sur un formulaire multi-étapes
        if(step){
          var regex = /step(\d)/;
          var match = regex.exec(step);
          step = parseInt(match[1]);

          alert('Etape ' + step);
          // on peut envisager un switch pour gérer chaque étape.
        }
      }
    }
  }
})(jQuery, Drupal, this, this.document);

Si vous vous aventurez dans la réalisation d'un tel formulaire avec javascript, je serai très interessé par vos retours et vos solutions pour bien gérer l'execution JS.

Accéder aux données de la session

Le fait de travailler avec les sessions permet d'accéder à tout moment aux valeurs du formulaire. Ceci m'a été particulièrement utile lors de la génération d'une fiche de résumé des saisies en PDF. Pour accéder aux valeurs stockées :

$valeurs = $_SESSION['clefduformulaire']

Ce sera tout pour aujourd'hui concernant les formulaires multi-étapes sous Drupal, il existe d'autres méthodes utilisant le module webform ou en construisant directement avec l'API form de Drupal, avez-vous essayé ces autres méthodes ? Qu'en pensez-vous ?

Par AlanT
Alan Tondelier

Fournir un flux XML et RSS valide à partir d&#039;une vue Drupal

Il vous est peut-être arrivé de rencontrer des erreurs de validation de vos flux RSS, ou de tout flux XML générés avec des vues Drupal. Cet article propose des pistes de réponses pour assurer leur validation. A noter que je m'intéresse ici à une solution Drupal, cependant cette démarche est tout à fait adaptable à d'autres CMS/technologies.

Caractères interdits et sections CDATA

Une histoire de norme Unicode

Les flux XML doivent se contraindre à des standards établis par le w3c, parmis ces standards il existe une norme qui va nous intéresser tout particulièrement pour nos sites Drupal : celle concernant l'encodage des caractères de nos flux. Celle-ci précise que tous les caractères à l'intérieur de vos balises xml doivent se plier à la norme ISO/IEC 10646.

Il arrive que certain caractères saisis dans vos contenus, ne valident pas la norme ISO/IEC 10646. Ceci arrive notemment lorsque l'on créé des contenus à partir de copier/coller sauvages à partir d'un logiciel tier. Conséquence directe, notre flux ne passe pas la validation et une belle erreur est listée sur le validateur w3c :

Line 79, Column 68: non SGML character number 31

Nous allons voir comment chasser ces caractères indésirables.

Utilisation des sections CDATA pour assurer la compatibilité de vos flux

Une erreur classique dans la génération d'un flux xml est la présence de markup html dans les balises XML.
Pour éviter ce problème, Drupal converti en entitées HTML le contenu des champs lors de la génération de l'affichage de votre vue (et donc de votre flux). 

Cependant certaines visionneuses de flux ou logiciels d'importation peuvent avoir du mal à gérer les entitées HTML et se comportent mieux avec du code HTML "brut". Pour permettre l'inclusion de balises HTML dans notre XML, une solution toute simple est d'utiliser les sections CDATA :

  &lt;item&gt;
    &lt;title&gt;Titre de mon article&lt;/title&gt;
    &lt;link&gt;http://blog.alantondelier.net/lien&lt;/link&gt;
    &lt;description&gt;
      &lt;![CDATA[ &lt;h1&gt;Ceci est un titre&lt;/h1&gt; &lt;img src="http://blog.alantondelier.net/uneimage.jpg" alt="Une image dans mon flux" /&gt; &lt;p&gt;Blabla&lt;/p&gt; ]]&gt;
    &lt;/description&gt;
  &lt;/item&gt;

Couplées à la fonction html_entity_decode pour retrouver notre markup, les sections CDATA permettent d'assurer la compatibilité de nos flux XML tout en laissant le code valide.

Mise en place de la solution

Intéressons-nous à la mise en place d'une solution pour filtrer les mauvais caractères et pour ajouter les sections <[CDATA[]]>.

Nous allons intervenir au niveau du theming Drupal, et plus particulièrement au niveau du template de views générant le code pour chaque élément du flux.
Prenons l'exmple le plus courant : celui d'un flux RSS basique généré par views. Pour les autres cas la méthode présentée est à adapter selon vos besoins. 

Pour les flux RSS, le template appelé sera views-view-row-rss.tpl.php. Placez une copie de ce template dans votre theme et ouvrez le :

&lt;?php

/**
* @file
* Default view template to display a item in an RSS feed.
*
* @ingroup views_templates
*/
?&gt;
  &lt;item&gt;
    &lt;title&gt;&lt;?php print $title; ?&gt;&lt;/title&gt;
    &lt;link&gt;&lt;?php print $link; ?&gt;&lt;/link&gt;
    &lt;description&gt;&lt;?php print $description; ?&gt;&lt;/description&gt;
    &lt;?php print $item_elements; ?&gt;
  &lt;/item&gt;

Nous allons commencer par modifier le template pour décoder l'HTML et ajouter une section CDATA dans notre balise <description> (il est possible d'effectuer la même opération sur <title> si vous y affichez de l'html "brut").

    &lt;description&gt;
      &lt;![CDATA[ &lt;?php print html_entity_decode($description); ?&gt; ]]&gt;
    &lt;/description&gt;

Il faut également retirer tous les caractères interdits par la norme XML. On utilise une fonction PHP de traitement de chaîne, empruntée ici, que l'on ajoute au template.php de notre theme :

/**
* Removes invalid XML
*
* @access public
* @param string $value
* @return string
*/

function montheme_xmlcleaner($value)
{

    $ret = "";
    $current;
    if (empty($value))
    {
        return $ret;
    }

    $length = strlen($value);
    for ($i=0; $i &lt; $length; $i++)
    {
        $current = ord($value{$i});
        if (($current == 0x9) ||
            ($current == 0xA) ||
            ($current == 0xD) ||
            (($current &gt;= 0x20) &amp;&amp; ($current &lt;= 0xD7FF)) ||
            (($current &gt;= 0xE000) &amp;&amp; ($current &lt;= 0xFFFD)) ||
            (($current &gt;= 0x10000) &amp;&amp; ($current &lt;= 0x10FFFF)))
        {
            $ret .= chr($current);
        }
        else
        {
            $ret .= " ";
        }
    }
    return $ret;
}

On termine par ajouter l'appel à cette fonction dans notre template views-view-row-rss.tpl.php :

&lt;?php

/**
* @file
* Default view template to display a item in an RSS feed.
*
* @ingroup views_templates
*/
?&gt;
  &lt;item&gt;
    &lt;title&gt;&lt;?php print montheme_xmlcleaner($title); ?&gt;&lt;/title&gt;
    &lt;link&gt;&lt;?php print $link; ?&gt;&lt;/link&gt;
    &lt;description&gt;
      &lt;![CDATA[ &lt;?php print montheme_xmlcleaner(html_entity_decode($description)); ?&gt; ]]&gt;
    &lt;/description&gt;
    &lt;?php print $item_elements; ?&gt;
  &lt;/item&gt;

La magie opère : votre flux xml est valide. 

N'hésitez pas à partager vos expériences sur les flux xml & Drupal en commentaire ou si vous avez rencontré un problème différent sur le sujet.

Par AlanT
Alan Tondelier

Installer et personnaliser le module CKEditor pour Drupal 7

Installer et personnaliser le module CKEditor pour Drupal 7

La mise en place du module ckeditor pour Drupal peut s'avérer plus compliquée que prévue. Ce rapide guide liste les étapes à suivre pour installer sans encombres le module. Une seconde partie s'intéresse à l'installation de plugins complémentaire étendant les fonctionnalités de cet éditeur wysiwyg de qualité.

Ajouter ckeditor à votre Drupal

Récupération des fichiers

Tout commence par une petite commande drush bien sentie. Si vous ne connaissez pas drush, faites vrombir votre curseur au plus vite pour le récupérer, cet utilitaire en ligne de commandes est un indispensable du développement Drupal (j'y reviendrai). Attention les commandes ci-dessous ne fonctionnent qu'avec les dernières versions de drush, pour les plus anciennes vous devez d'abord télécharger le module (avec le switch "dl").

drush en ckeditor -y

Je vous conseille également de récupérer ckeditor_link qui vous permettra de créér des hyperliens vers vos contenus Drupal directement via l'interface de Ckeditor, indispensable !

drush en ckeditor_link -y

Une fois la récupération des modules faite, rendez vous sur http://ckeditor.com/download pour télécharger la librairie ckeditor.

Une petite personnalisation SEO-friendly

Afin de fournir du code plus moderne, apprécié des moteurs de recherches mais également pour fournir plus d'accessibilité à vos contenus, j'ai l'habitude d'effectuer une petite personnalisation de ckeditor avant de le télécharger : ajouter le support de la balise <figure> et <figcaption>.

Sur la page de ckeditor, prenez l'option "Or let met customize CKEditor" puis le bouton rouge.
Prendez le preset "Full" puis dans la partie basse, colonne de droite trouvez "Enhanced Image" et basculez-le colonne de gauche.

Si vous suivez cette étape, les  images que vous chargerez via CKEditor pourront être enrichies de la sémantique HTML5.


Le plugin Enhanced Image ajoutera les balises html5 figure et figcaption.

Ajoutez ensuite toutes les langues à ckeditor (j'ai rencontré des problèmes en ne choisissant que les langues utilisées sur mes sites), et récupérez votre librairie.

Une fois votre CKEditor téléchargé, placez le contenu de l'archive dans votre dossier /sites/all/modules/contrib/ckeditor/ckeditor .

CKFinder pour gérer vos fichiers

CKEditor ne sait gérer seul l'upload de fichiers sur votre serveur, pourtant il est très utile de pouvoir ajouter des visuels ou attacher des fichiers directement dans le corps de votre texte.
Heureusement, il existe des plugins pour remédier à ce manque. 

Le plus connu et utilisé d'entre eux se nomme CKFinder (payant mais possède une version de démonstration avec quelques limitations pour un usage non commercial).

Pour l'ajouter à votre CKEditor, les étapes sont les suivantes :

  1. Récupérez le plugin ici,
  2. Drupal cherchera par défaut ckfinder dans le répertoire /sites/all/modules/contrib/ckeditor/ckfinder. Dézippez-le à cet emplacement.
  3. Pour que le système de droits de Drupal gère correctement les permissions de ckeditor, vous devez modifier le fichier config.php de CKFinder.

Supprimez ces lignes (21-34) :

function CheckAuthentication()
{
    // WARNING : DO NOT simply return "true". By doing so, you are allowing
    // "anyone" to upload and list the files in your server. You must implement
    // some kind of session validation here. Even something very simple as...

    // return isset($_SESSION['IsAuthorized']) &amp;&amp; $_SESSION['IsAuthorized'];

    // ... where $_SESSION['IsAuthorized'] is set to "true" as soon as the
    // user logs in your system. To be able to use session variables don't
    // forget to add session_start() at the top of this file.

    return false;
}

Et remplacez les par 

require_once '../../../../includes/filemanager.config.php';
  1. Finissez par éditer la variable $cookie_domain de votre settings.php
 $cookie_domain = '.votrenomdedomain.ext';

Votre CKFinder est maintenant fonctionnel.

Configuration de CKEditor

Les bases

Par défaut CKEditor a créé deux profils à partir des formats de textes installés par défaut (lors d'une mise en route standard de Drupal). Nous allons voir brievement la configuration d'un de ces profils.

La configuration des profils se fait dans admin/config/content/ckeditor/.

Modifiez l'un des profil qui vous intéresse et dans "Apparence de l'éditeur", choisissez les boutons de barre d'outil à faire apparaitre pour ce profil. Si vous souhaitez de plus activer le CKEditor Link, cochez la case dans la sous rubrique Plugins ET rendez vous dans la configuration du format de texte correspondant pour activer le filtre CKEditor Link afin d'avoir de belles URLs.

Je vous conseille également de limiter les balises dans "Nettoyer et afficher" > "Formats de polices de caractères". Bien souvent la balise H1 est inutile car déjà présente pour le titre de la page, d'autres balises peuvent selon vos cas être inutiles ou source de confusion pour les utilisateurs, à vous de faire le tri.

N'oubliez pas non plus d'activer CKFinder dans les "paramètres de l'explorateur de fichiers".

Une fois ces réglages effectués, CKEditor devrait être visible dans vos zones de textes possédant un format de texte associé à un profil.

Configurations avancées

Très vite, il devient intéressant de pouvoir définir ses propres styles dans ckeditor. Pour ce faire, dans les réglages d'un profil, dans CSS puis styles prédéfinis, définissez le chemin de ckeditor.styles.js, j'ai l'habitude d'utiliser cette syntaxte "%tjs/ckeditor.styles.js" pour appeler le fichier dans le theme principal du site (et non celui de l'administration comme le suggère Drupal !).

L'appel dans le thème évite de supprimer le fichier lors d'une mise à jour du module ou de la librairie.

Vous trouverez la copie originale de ckeditor.style.js dans le répertoire du module CKEditor. Voici un extrait du ckeditor.styles.js que j'utilise sur ce blog :

/*
Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
/*
* This file is used/requested by the 'Styles' button.
* The 'Styles' button is not enabled by default in DrupalFull and DrupalFiltered toolbars.
*/
if(typeof(CKEDITOR) !== 'undefined') {
    CKEDITOR.addStylesSet( 'drupal',
    [
            /* Block Styles */             // code wrapper             {
                name: 'Code Wrapper',
                element: 'pre',
            },             /* Inline Styles */
            // These are core styles available as toolbar buttons. You may opt enabling
            // some of them in the "Styles" drop-down list, removing them from the toolbar.
            { name : 'Bleu'  , element : 'span', attributes : { 'class' : 'bleu' } },
            { name : 'Orange'  , element : 'span', attributes : { 'class' : 'orange' } },
            { name : 'Big'        , element : 'big' },
            { name : 'Small'      , element : 'small' },
            // code
            {
                    name : 'Code PHP',
                    element : 'code',
                    attributes :
                    {
                            'class' : 'language-php',
                    }
            },
            {
                    name : 'Code CSS',
                    element : 'code',
                    attributes :
                    {
                            'class' : 'language-css',
                    }
            },
            {
                    name : 'Code HTML',
                    element : 'code',
                    attributes :
                    {
                            'class' : 'language-markup',
                    }
            },
            {
                    name : 'Code JS',
                    element : 'code',
                    attributes :
                    {
                            'class' : 'language-javascript',
                    }
            },
...etc

Étendre CKEditor

Ajout de plugins

La communauté de CKEditor a créé un certain nombre de plugins téléchargeables sur cette page : http://ckeditor.com/addons/plugins/all.

Ces plugins s'incorporent directement au ckeditor de votre Drupal en téléchargeant l'archive du plugin, et en l'extrayant deux fois, dans le dossier /modules/contrib/ckeditor/plugins et dans /modules/contrib/ckeditor/ckeditor/plugins. (En réalité il n'est pas nécessaire de tout extraire en double, mais sachez que cette méthode est "sûre").

Une fois vos plugins ajoutés, leur activation se fait dans "Apparence de l'éditeur" > "Plugins".

Création de plugin

Il est évidemment possible de créer son propre plugin CKEditor au sein même de Drupal, je reviendrai sur ce point lors d'un prochain billet.

 

Par AlanT
Alan Tondelier

Lancement du Blog !

Lancement du Blog !

Enfin !
Après plusieurs semaines de questionnement personnel ponctuées principalement par des "faut-il ?", le Blog est finalement en ligne.

Qu'attendre de ce blog ?

Ce blog est né de ma volonté de partager des connaissances acquises au cours des 3 dernières années sur Drupal.
En effet, au fil des semaines passées a approfondir le CMS Drupal, j'ai été à plusieurs reprises surpris de ne pas lire plus d'articles sur des problèmes récurrents, rencontrés par de nombreuses personnes de la communauté et qui ne trouvent que des réponses partielles, incomplètes ou "alambiquées".

Sans avoir la prétention d'apporter des réponses "ultimes" ou parfaites, je veux avant tout partager avec ceux qui me liront (coucou !) des pistes cohérentes, fonctionnelles et permettant, je l'espère, d'apporter une brique de qualité à l'édifice Drupal.

Le blog d'un autodidacte sur la scène Drupal & Web

Qui suis-je ?
Pour faire court, je suis un ingénieur diplômé de Grenoble-INP - Pagora filière communication imprimée. Ayant toujours côtoyé de près les sites Internet, depuis l'époque où couinaient les modems 56K, j'ai en 2011 entrepris de tenter l'aventure web, d'abord à Paris puis à Rouen en Haute Normandie.
Après près de 3 années en agence, j'ai finalement décidé de prendre mon indépendance.

Je ponctuerai ce blog de quelques articles plus personnels afin de donner quelques retours d'expérience sur cette aventure numérique.

Une promesse avec moi-même

Il est très facile de ne pas tenir la rédaction d'un blog, aussi j'essaierai dans la mesure du possible de poster un article au minimum par semaine afin de faire vivre cet espace :)

Blog ... GO !

 

Par AlanT
Alan Tondelier

Générer et personnaliser ses PDF avec Drupal et dompdf

Pour générer des fichiers PDF des pages Drupal, il existe le module "Print", qui contient le sous module "print_pdf". Ce module permet de convertir très simplement en PDF toute page possédant une URL interne. Print propose même un système de templates permettant de peaufiner l'affichage des PDF pour chaque type de nodes.

Cependant, il arrive d'avoir besoin de générer des PDF dont l'affichage diffère complètement de la page drupal associée et où des données non présentes sur la page doivent être récupérées. Cet article explique comment parvenir a ses fins en détournant un petit peu le module print. Cet article s'appuie sur la version 2.x du module.

Quelle librairie PDF choisir ?

Présentation

Lorsque vous installez le module print et que vous activez la génération de pdf (module print_pdf) il vous est demandé de déposer une librairie PDF dans votre Drupal. Il existe à ce jour 4 librairies PDF supportées par print pour générer vos PDF :

  1. dompdf
  2. TCPDF
  3. mPDF (avec la version 2.x de print)
  4. wkhtmltopdf

Chaque librairie possède ses avantages/inconvénients et leur choix va dépendre énormément de votre projet. Une étude comparative de ces 4 libraries est disponible sur fuseinteractive.ca . 

Personnellement, j'ai trouvé TCPDF trop "mauvais" dans la gestion des CSS. La mise en place de whtmltopdf délicate car on n'a pas toujours l'accès root du serveur qui héberge le site, même si c'est la solution la plus puissante (et la seule vraiment valable pour vos très gros pdf).

Concernant dompdf et mPDF, pour généraliser, dompdf a un meilleur support CSS que mPDF mais est moins performant que ce dernier.

J'ai eu l'occasion de pousser dompdf assez loin pour générer des pdf plutôt complexes : http://egrid.epg-project.eu/fr/egrid : 10 langues avec différents jeux de caractères, la possibilité d'ajouter un glossaire dans 4 langue de son choix, une numérotation des pages et un footer personnalisé.

Génération de PDF complexe avec Drupal
Extrat d'un PDF dynamique complexe avec Drupal avec footer personnalisé et numérotation de pages 

Pour la suite de cet article, j'utiliserai la bibliothèque dompdf pour générer mes fichiers.

Dompdf

Installer la librairie dompdf

Pour installer simplement la librairie dompdf, il vous faut :

  1. Récupérer la dernière version de dompdf : https://github.com/dompdf/dompdf/tags,
  2. Récupérer php-font-lib 0.2.2 : https://github.com/PhenX/php-font-lib/tree/0.2.2,
  3. Si vous ne l'avez pas, le module libraries,
  4. Dézipper dompdf dans sites/all/libraries/dompdf,
  5. Dézipper php-font-lib dans sites/all/libraries/dompdf/lib/php-font-lib,
  6. v2.0 du module : veillez à activer également le module "dompdf library handler".

Dompdf est installé, vous pouvez peaufiner vos réglages dans <admin>/config/user-interface/print .

Générer et personnaliser des PDF "sur-mesure" avec Dompdf

Ajouter des templates à n'importe quelle page possédant un lien interne

Le module print permet la personnalisation de templates selon le modèle hiérarchique suivant ( 1 sera prioritaire sur 2 qui sera prioritaire sur 3, etc...) : 

  1. print--[format]--node--[type]--[nid].tpl.php,
  2. print--[format]--node--[type].tpl.php,
  3. print--[format].tpl.php,
  4. print--node--[type].tpl.php,
  5. print.tpl.php dans votre thème,
  6. print.tpl.php dans le module print (par défaut).

[format] est le format de sortie du module print (html, pdf ou mail), [type] est le nom interne du type de contenu, [nid] est l'ID du node.

Cependant, cette structure ne permet pas d'appliquer un template à n'importe quelle page de notre site. Par exemple, comment faire pour ajouter un template PDF personnalisé pour une page générée par views ? Sans pour autant écraser le template général ?

Pour remédier à cela, vous pouvez utiliser hook_preprocess_print() :

template.php de votre theme

/**
* Implements hook_preprocess_print().
*/

function demo_preprocess_print(&amp;$vars){

  // add support for template files
  // print--[format]--path-[drupal-path].tpl.php
  // and
  // print--path-[drupal-path].tpl.php
  // with recursive path suggestions

  $cpath = current_path();
  $crumbs = explode('/',$cpath);

  // if first crumb is "print" remove it

  if($crumbs[0] == "print" ){ array_shift($crumbs); }

  $vars['theme_hook_suggestions'][] =  "print__path_${crumbs[0]}";
  $vars['theme_hook_suggestions'][] =  "print__${vars['format']}__path_${crumbs[0]}";

  // if there is more than one crumb
  if(count($crumbs) &gt; 1){

    // remove crumb already used
    array_shift($crumbs);

    foreach ($crumbs as $key =&gt; $crumb) {

      // get number of current theme suggestions
      $size = count($vars['theme_hook_suggestions']);

      // add crumb to theme suggestions
      $vars['theme_hook_suggestions'][] = $vars['theme_hook_suggestions'][$size-2]."_${crumbs[$key]}";
      $vars['theme_hook_suggestions'][] = $vars['theme_hook_suggestions'][$size-1]."_${crumbs[$key]}";

    }

  }

}

Ainsi, grâce a cette fonction, je peux facilement mettre en place un template pour personnaliser la sortie de mon pdf.

Si le chemin de ma vue est "mavue", le template PDF sera : print--pdf--path-mavue.tpl.php ou print--path-mavue.tpl.php (pour personnaliser à la fois le PDF et l'affichage pour impression).

Plus intéressant la fonction gère également les chemins à plusieurs arguments :
Par exemple si mon chemin est mavue/arg2, le template PDF pourra être (du plus selectif au moins selectif) :

  1. print--pdf--path-mavue-arg2.tpl.php,
  2. print--path-mavue-arg2.tpl.php,
  3. print--pdf--path-mavue.tpl.php,
  4. print--path-mavue.tpl.php

Ajouter un footer personnalisé à vos PDF et les numéros de pages...

Pour ajouter des informations en pied de page de vos PDF, vous devez tout d'abord activer l'utilisation de PHP dans dompdf. 
Rendez vous dans libraries/dompdf/dompdf_config.custom.inc.php et décommentez la ligne 12

define("DOMPDF_ENABLE_PHP", true);

Maintenant dans le template qui vous intéresse, rajoutez le code suivant après l'ouverture de la balise <body>

    &lt;script type="text/php"&gt;
      if (isset($pdf)) {
        $font = Font_Metrics::get_font("verdana");;
        $size = 10;
        $color = array(0,0,0);
        $text_height = Font_Metrics::get_font_height($font, $size);

        $w = $pdf-&gt;get_width();
        $h = $pdf-&gt;get_height();

        $footer = $pdf-&gt;open_object();

        // Draw a line along the bottom
        $y = $h - 25;
        $pdf-&gt;line(15, $y, $w - 15, $y, $color, 1);

        $y += $text_height / 2;
        $pdf-&gt;page_text(15, $y, 'Texte de mon footer', $font, $size, $color);

        $pdf-&gt;close_object();
        $pdf-&gt;add_object($footer, "all");

        // Center the text
        $width = Font_Metrics::get_text_width("Page 1 of 2", $font, $size);
        $pagenumtxt = t("Page !n of !total", array("!n" =&gt; "{PAGE_NUM}", "!total" =&gt; "{PAGE_COUNT}"));
        $pdf-&gt;page_text($w - 15 - $width, $y, $pagenumtxt, $font, $size, $color);
      }
    &lt;/script&gt;

Le code ci-dessous rajoutera une ligne, le texte "Texte de mon footer" et la numérotation des pages à droite. A vous d'adapter ce code pour coller à vos besoins. Pour plus d'options de mises en page, vous pouvez vous pencher sur l'API de dompdf .

J'espère que cet article vous fera gagner du temps lors de la mise en place de PDF avancés, il y aurait encore beaucoup de choses à dire sur le sujet, aussi si vous avez des remarques n'hésitez pas à les donner en commentaires !

Par AlanT
Alan Tondelier

Simplifiez et accélérez vos installations Drupal avec Drush make

Drush est un outil en ligne de commande permettant d'administrer simplement Drupal. Disponible sur toutes les plateformes (unix/osx/windows), il est un indispensable du développement de vos sites.

La commande make de Drush permet de préparer rapidement une installation de Drupal avec ses modules contribués, ses librairies pré-téléchargées et plus encore. Incontournable quand on travaille sur un grand nombre de projets où l'on utilise à 80% les mêmes modules.

Une installation Drupal prête à l'emploi

C'est quoi Drush make ?

La commande drush make permet de récupérer les sources de projets Drupal (core, module, themes...) à partir des dépots officiels. Afin de décrire quels éléments récupérer, la commande make doit être accompagnée d'un fichier .make. Description et exemple d'un fichier .make :

mydrupal.make

; Base Drupal
core = 7.x

; Internal API for Drush make
api = 2

; Drupal Version
projects[drupal][version] = "7.26"

; Modules
; If no version precised, fetch the last stable one
; see projets[views] for an example

projects[admin_menu][subdir] = "contrib"
projects[adminimal_admin_menu][subdir] = "contrib"
projects[backup_migrate][subdir] = "contrib"
projects[ctools][subdir] = "contrib"
projects[ckeditor][subdir] = "contrib"
projects[ckeditor_link][subdir] = "contrib"
projects[context][subdir] = "contrib"
projects[devel][subdir] = "contrib"
projects[google_analytics][subdir] = "contrib"
projects[jquery_update][subdir] = "contrib"
projects[libraries][subdir] = "contrib"
projects[metatag][subdir] = "contrib"
projects[pathauto][subdir] = "contrib"
projects[pathologic][subdir] = "contrib"
projects[token][subdir] = "contrib"
projects[transliteration][subdir] = "contrib"
projects[variable][subdir] = "contrib"

; Declare the use of version 3.6 of views
projects[views][subdir] = "contrib"
projects[views][version] = "3.6"
projects[webform][subdir] = "contrib"

; Themes
projects[adminimal_theme][type] = "theme"
projects[mothership][type] = "theme"

; Libraries
; Please fill the following out. Type may be one of get, git, bzr or svn,
; and url is the url of the download.

libraries[lessphp][download][type] = "file"
libraries[lessphp][download][url] = "http://leafo.net/lessphp/src/lessphp-0.4.0.tar.gz"
libraries[lessphp][directory_name] = "lessphp"
libraries[lessphp][type] = "library"

libraries[ckeditor][download][type] = "file"
libraries[ckeditor][download][url] = "http://ckeditor.com/online-builder/releases/minified/4.3.3/moono/4.3.3/ckeditor_4.3.3.zip"
libraries[ckeditor][directory_name] = "ckeditor"
libraries[ckeditor][destination] = "modules/contrib/ckeditor"

Comme vous pouvez le voir la structure est très simple. Le fichier commence par déclarer la version de Drupal à utiliser, liste les projets eque vous souhaitez automatiquement télécharger ainsi que les thèmes. Les fichiers .make peuvent également récupérer directement les librairies dont vous avez besoin. 

Une fois ce fichier créé et enregistré, il vous suffit d'invoquer Drush avec cette syntaxe :

drush make mydrupal.make mywebsite -y

Drush créera votre installation de Drupal dans le dossier mydrupal, le switch -y répondra par l'affirmative à tous les prompts de la console.

Drush make console
Sortie de drush make sur un environnement windows

Le téléchargement des fichiers terminé, vous vous retrouvez avec votre dossier Drupal prêt à l'utilisation, vous n'avez plus qu'à executer install.php.

Pour une liste exhaustive des fonctionnalités de drush make, vous pouvez lire la documentation officielle (en anglais) ici

Drush make VS profil d'installation

Il est légitime de se poser la question : "Pourquoi ne pas passer par un profil d'installation Drupal ?"

Les fichiers make possèdent un certain avantage face aux profils d'installation :

  1. Vous récupérez automatiquement la dernière version stable du module,
    Tout en gardant la possibilité de préciser la version d'un module en particulier en rajoutant la ligne "projects[nomprojet][version] = "7.2", où 7.2 est la version de votre module.
  2. Il est plus facile de maintenir un fichier make qu'un ensemble de dossiers de modules, de plus vous pouvez facilement mettre en place des fichiers make pour chaque typologie de projets : site_corporate.make, site_multilingue.make, site_vitrine.make ...
  3. L'utilisation de drush make permet d'automatiser facilement le déploiment de Drupal dans des scripts bash (unix) ou batch (windows). J'y reviendrai.

Le principal point noir de Drush make est sa relative lenteur pour récupérer les fichiers des serveurs Drupal, là où un profil d'installation possède déjà tous les fichiers à installer.

Générer facilement ses fichiers .make

La méthode manuelle & semi-manuelle

Pour générer vos fichiers .make, vous pouvez simplement ouvrir votre IDE favori et taper à la main le fichier. À préférer lorsque vous savez exactement ce que vous voulez.

Une autre façon de faire est d'utiliser le site http://drushmake.me qui vous assistera dans la création de vos fichiers .make. Pratique !

Génération du .make à partir d'un site Drupal existant

Vous avez un site Drupal "référence" et vous souhaiteriez en sortir un fichier .make ? C'est possible !
Avec drush, placez vous dans le répertoire Drupal du site considéré puis :

drush generate-makefile /chemin/du/fichier-make.make

Et votre fichier .make sera automatiquement généré. Selon votre projet, des informations manquantes peuvent survenir, notamment si vous avez développé des modules custom. Éditez votre fichier .make en conséquence de façon à avoir une base propre de déploiement. 

Remarque : En utilisant generate-makefile, la version de chaque module sera précisé dans le fichier .make, de façon à avoir un Drupal à jour je vous conseille de supprimer les lignes précisant la version de vos modules.

Ainsi, avec Drush make, vous accélérerez considérablement votre mise en place de projets Drupal, en local ou sur vos serveurs.

 

Par AlanT
Alan Tondelier

Personnalisez jusqu&#039;à la moelle le template de vos field-collections

Troll matryoshka

Le module field_collections permet de créer des "champs de champs", particulièrement pratique pour accélérer la création de contenus et permettre de simplifier la gestion en back-office. Si vous avez déjà travaillé avec field-collection, vous avez pu vous heurter à un problème lors de la modification de la sortie HTML, ou bien essayé de "themer" différement un des items de votre field-collection. 
Cet article propose un template regroupant l'ensemble de la sortie html d'un field-collection, permettant ainsi d'en simplifier la personnalisation.

Note : Cet article est une réponse "simpliste" à un problème de la communauté Drupal datant du 14 Mai 2011 : https://drupal.org/node/1157794, actuellement la solution proposée n'a pas encore été implémentée. Elle le sera dans la version 2.x de field-collection qui rendra cet article obsolète.

Field Collection, anatomie d'un theming compliqué


Sortie par défaut de field-collection avec formatteur "Field collection items"

Lorsque vous mettez en place un field-collection, drupal va rendre l'html de sortie en passant par ces étapes :

  1. Ajout de l'html pour les variables (modifier/supprimer/ajouter) directement dans une fonction de theming interne au module field_collection,
  2. Passage au fichier de template field.tpl.php pour mettre en place l'html d'enveloppe du champ,
  3. Passage au fichier de template field-collection-item.tpl pour rendre l'html de chaque item de field-collection.

Donc, si je veux personnaliser entièrement la sortie html de mes field-collection , parce-que j'ai envie de faire une intégration html aux petits oignons, la mission va s'avérer assez délicate.

Une solution "maison"

Ce n'est pas la plus élégante des solutions !

Avant tout, la solution proposée n'a pas vocation de remplacer une vraie modification du coeur du module field-collection, bien au contraire, ce que je propose est à mi chemin entre un "hack" et une utilisation conventionnelle des fonctions de theming de Drupal.
Aussi vous pouvez regarder du côté du topic officiel sur drupal.org pour un patch du module permettant (normalement) d'arriver au même résultat.

Cette solution a cependant l'avantage de faire le travail demandé, à savoir personnaliser au maximum la sortie de vos field-collection mais également d'être applicable sans aucune modification de module ou d'installations complémentaires ; en travaillant sur le template.php de votre thème et en modifiant deux fichiers .tpl.php.

Et dès que la version 2.x de field_collection sera sortie, le problème ne se posera plus.

En attendant...

Nous allons travailler dans template.php, remplacez "votre_theme" par le nom système de votre thème "front" :

template.php - hook_preprocess_node()

/**
* Override or insert variables into the node template.
*/
function votre_theme_preprocess_node(&amp;$variables) {
  if ($variables['view_mode'] == 'full' &amp;&amp; node_is_page($variables['node'])) {
    $variables['classes_array'][] = 'node-full';
  }

  // Field Collection Alter - This is required in order to pass the description of the field collection and the "add new item" to $variables.

  if (!empty($variables['content'])) {
    foreach ($variables['content'] as $key =&gt; $field) {

      if (isset($field['#field_type']) &amp;&amp; $field['#field_type'] === 'field_collection' &amp;&amp; $field['#formatter'] === 'field_collection_view') {

        // move description and links from #suffix into separate variables for field_collection_view;
          if ($field['#formatter'] === 'field_collection_view') {

            // remove #prefix - if not, get printed
            $variables['content'][$key]['#prefix'] = '';

            //capture links and description - this is hackish (but works) !
            preg_match("/&lt;div .*&gt;(.*)&lt;\/div&gt;./i", $field['#suffix'], $desc);
            preg_match("/(&lt;ul.*&lt;\/ul&gt;)/i", $field['#suffix'], $links);

            // field_collection global description
            if (isset($desc[1])) {
             $variables['content'][$key]['fc_description'] = $desc[1];
            }

            // field_collection links (add new item to the collection)
            if (isset($links[1])) {
              $variables['content'][$key]['fc_links'] = $links[1];
            }

            //remove #suffix - if not, get printed
            $variables['content'][$key]['#suffix'] = '';

          }

      }

    }

  }

}

template.php - hook_preprocess_field

/**
* Override or insert variables into the field template.
*/
function votre_theme_preprocess_field(&amp;$vars) {

  if (isset($vars['element']['#field_type']) &amp;&amp; $vars['element']['#field_type'] === 'field_collection' &amp;&amp; $vars['element']['#formatter'] === 'field_collection_view') {

    // move each item into a renderable array and pass html for edit and delete links into simple variables

    foreach ($vars['items'] as $key =&gt; &amp;$item) {
      // item
      $item['item'] = array_shift($item['entity']);

      // edit
      if (isset($item['links']['#links']['edit'])) {
        $edit = $item['links']['#links']['edit'];
        $edit = l($edit['title'], $edit['href'], array('query' =&gt; $edit['query'],'attributes'=&gt;array('class'=&gt;'field-collection-edit-item')));
        $item['item']['#edit_link'] = $edit;
      }

      // delete
      if (isset($item['links']['#links']['delete'])) {
        $delete = $item['links']['#links']['delete'];
        $delete = l($delete['title'], $delete['href'], array('query' =&gt; $delete['query'],'attributes'=&gt;array('class'=&gt;'field-collection-delete-item')));
        $item['item']['#delete_link'] = $delete;
      }

      // pass a nice, renderable array to template file
      $item = $item['item'];

    }

    // add field-collection description
    if (isset($vars['element']['fc_description'])) {
      $vars['fc_description'] = $vars['element']['fc_description'];
    }
   
    // add field-collection links
    if (isset($vars['element']['fc_links'])) {
      $vars['fc_links'] = $vars['element']['fc_links'];
    }

  }
}

Ainsi, nous avons modifié les données transmises au fichiers templates. Les fichiers templates utilisés pour rendre les field-collection n'ont pas changé, il s'agit toujours de field.tpl.php pour l'enveloppe et field-collection-item.tpl.php pour chaque item de la collection. Or avec ce qui a été fait dans template.php, field-collection-item.tpl.php doit renvoyer la plus simple des structure (ie ne pas ajouter d'html).

field-collection-item.tpl.php - à placer dans votre thème

&lt;?php
  print render($content);
?&gt;

Maintenant la partie qui nous intéresse le plus, le template de rendu de votre field-collection : field.tpl.php. Si vous avez suivi le code, vous avez du voir que nous avons rendu disponible le contenu normalement affiché dans field-collection-item directement au niveau de field.tpl.php.

Votre template field.tpl.php est utilisé pour rendre l'html de tout les champs de votre site, vous cibler uniquement les field-collection, nommez le field--field-collection.tpl.php, vous pouvez également mettre différents templates par field-collection et par affichage selon la syntaxe :

  1. field--[nom_systeme_du_field].tpl.php
  2. field--[nom_systeme_du_field]--[nom_systeme_affichage].tpl.php

field--field-collection.tpl.php - à placer dans votre thème

<code class="language-markup">&lt;!-- field-collection wrapper --&gt;
&lt;div class="field-collection"&gt;

&lt;?php if (!empty($fc_description)): // description of the field-collection ?&gt;

  &lt;div class="field-collection-description"&gt;

    &lt;?php print $fc_description; ?&gt;

  &lt;/div&gt;

&lt;?php endif; ?&gt;

&lt;?php foreach ($items as $delta =&gt; $item): // loop on each item of the collection ?&gt;

  &lt;div class="field-collection-item"&gt;
    &lt;?php
      print render($item);
      // or play with some fields
      // print render($fc_item['my_field']);
    ?&gt;

    &lt;?php if (isset($item['#edit_link']) || isset($item['#delete_link'])): // check that links are available (can be enabled/disabled in display settings) ?&gt;

      &lt;div class="field-collection-item-links"&gt;
        &lt;?php print isset($item['#edit_link'])?$item['#edit_link']:NULL; ?&gt;
        &lt;?php print isset($item['#delete_link'])?$item['#delete_link']:NULL; ?&gt;
      &lt;/div&gt;

    &lt;?php endif; ?&gt;

  &lt;/div&gt;

&lt;?php endforeach; ?&gt;

  &lt;?php print !empty($fc_links)?$fc_links:NULL; // link for adding a new item to the collection ?&gt;

&lt;/div&gt;
&lt;!-- end field-collection wrapper --&gt;

Videz vos caches, et normalement votre nouveau template prend le relai dans l'affichage de vos field-collections. À noter que ce template est utilisé uniquement lors de l'affichage "field collection items".


Veillez à mettre le format de sortie sur "field collection items", sinon le template par défaut sera utilisé.

J'espère que cet article vous sera utile, et n'hésitez pas à faire des retours si vous l'implémentez dans vos projets Drupal :)

Par AlanT
Alan Tondelier

Afficher n&#039;importe quel block dans le corps de vos articles Drupal

Par design, les blocks Drupal doivent être placés dans des régions, en-tête, contenu, footer, sidebar, ce mécanisme permet facilement de structurer et de réorganiser rapidement les sections de nos sites.

Cependant il arrive d'avoir besoin d'afficher rapidement un block à l'intérieur même du texte d'un article, ceci peut survenir par exemple lorsque l'on souhaite écrire un premier paragraphe, insérer un block issu d'une vue Drupal puis continuer la rédaction. Je vous présente une méthode simple pour arriver à ce résultat.

Une syntaxe pour les appeler

Afin de placer nos blocks dans le contenu texte des nodes, il faut trouver une solution simple et sécurisée. Le bourrin pourra appeler directement les blocks par du code PHP, mais ce comportement pourra induire de bonnes failles de sécurité.

La solution proposée ici est d'utiliser une chaîne avec une syntaxe prédéfinie qui sera traitée en aval :

{--!block!{module-qui-genere-le-block}{nom-systeme-du-block} # <em><strong>Ici un commentaire</strong> </em>! #--}

Ainsi, en plaçant cette chaîne à l'intérieur de vos textes on évite tout code php et la présence d'un commentaire permet d'indiquer à vos éditeurs ce que fait cette chaîne de caractère "étrange" (et leur évite ainsi d'y toucher...).

Un hook pour les générer

Il va falloir maintenant récupérer cette chaîne de caractère et travailler avec hook_preprocess_node afin de récupérer le block qui nous intéresse. À partir d'ici deux solutions :

  1. On traite notre chaîne dans le template.php du thème de rendu front,
  2. On met en place un module qui va traiter cette chaîne.

Évidemment, la seconde méthode est la meilleure : réutilisabilité du code, possibilité de construire une page d'administration du module (par exemple pour définir quels fields peuvent contenir des blocks et pas seulement le field body), gestion des droits, meilleure intégration dans l'éditeur wysiwyg, etc...

Cependant, mon but ici est avant tout de vous donner une piste qui fonctionne, aussi je vous laisse libre de décider ou traiter cette chaîne.

Dans les deux cas, tout se passe avec hook_preprocess_node :

hook_preprocess_node - ou hook est le nom de votre thème OU de votre module

function hook_preprocess_node(&amp;$variables, $hook) {

  if(isset($variables['content']['body'])){

  $regexGlobal = '/{--(?P&lt;capture&gt;.*)--}/';
  $regexMod = '/!(?P&lt;module&gt;[a-z]+)!/';
  $regexParams = '/{(?P&lt;params&gt;(\w|-)+)}/';
 
  // Get the body value, this can be extended to other fields
  $body = $variables['content']['body'][0]['#markup'];

    if (preg_match($regexGlobal, $body, $matches)) { // if special strings are found

      $capture = $matches['capture'];
      preg_match($regexMod, $capture, $module);

      switch ($module = $module['module']) {
        case 'block':
          // Block code, i used a switch here for extention purpose.
For instance we could also fetch a full view, with filters and sorting, with this method...
          preg_match_all($regexParams, $capture, $params); // get block params
          $params = $params['params'];

          // fetch block
          $block = block_load($params[0], $params[1]);
          // the following call ensure that rights, contextual links, and so are functionnal
          $block = _block_get_renderable_array(_block_render_blocks(array($block)));
          $block = drupal_render($block);

          // replace special string with block HTML
          $variables['content']['body'][0]['#markup'] = preg_replace($regexGlobal, $block, $body);

          break;

        case 'my_other_module':
          // do stuff
          break;

        default:
       
          break;
      }

    }

  }

}

Ainsi vous avez une méthode simple et facile à mettre en place pour afficher vos blocks directement dans vos contenus. Bien entendu ceci est un point de départ, à vous d'étendre ce code pour vos besoins.

Et dans un exemple les lier

Ce mécanisme est utilisé sur la page suivante : meetsys.com - les clients.

Vue de l'administration :


Affichage d'un block dans le corps du texte avec cette méthode

 

Par anavarre

Les modules sur lesquels parier dans le futur

Récemment, je vous ai donné un rapide aperçu des nouveautés et challenges que Drupal 8 allait introduire. Je vous expliquais entre autres que bon nombre de modules allaient vraisemblablement disparaître. Cela va des modules stables sous Drupal 6 qui n'ont jamais eu de version stable pour Drupal 7 et n'en auront donc apparemment jamais sous Drupal 8, mais aussi des modules qui ne franchiront tout simplement pas le pas entre D7 et D8, pour quelque raison que ce soit.

Par anavarre

Faut-il apprendre Drupal 8 maintenant ?

La question peut se poser car si l'on en croit la roadmap de développement, nous devrions pouvoir avoir une version stable à la fin de l'été 2013, pour autant que les bugs critiques aient été réglés. Autant dire que lorsque vous voudrez commencer un site dessus ou répondre à une demande client, il vaudra mieux être prêt vu l'éténdue des changements (c'est un euphémisme).

Par anavarre

Bilan 2012 et perspectives 2013

Voilà l'année 2013 qui commence et DrupalFacile qui fête quasiment ses deux ans et demi. Comme chaque année, c'est le moment de vous dévoiler quelques statistiques sur le site et vous allez voir que les choses progressent plutôt bien, preuve que la demande d'informations et d'aide sur Drupal croît constamment :

Par anavarre

Savez-vous vous servir de drupalcode.org ?

Sur chaque page de projet Drupal, plusieurs informations extrêmement importantes et/ou pratiques existent. L'une d'entre elles vous aura peut-être échappée et concerne le dépôt git du projet que vous pouvez manipuler en ligne de plusieurs façons. Vous y avez accès très facilement, en suivant le lien Repository viewer depuis le bloc Development dans la barre latérale de droite.

Par anavarre

Sauvegardez vos sites Drupal avec NodeSquirrel

Si vous suivez de près l'actualité Drupal, cela ne vous aura pas échappé. NodeSquirrel vient récemment d'ouvrir ses portes après une longue beta pour se placer comme LE spécialiste de la sauvegarde hors-site de vos sites Drupal. Il faut dire que l'équipe qui propose ce service est la même que celle derrière le module Backup and Migrate, ce qui lui donne une crédibilité certaine quand on connaît la popularité du module et à quel point il fonctionne bien.

Par anavarre

Présentation : Optimiser les performances Drupal par le cache

En janvier dernier, lors d'un vote sur les sujets que les membres de la communauté lyonnaise voulaient voir traiter dans les prochains meetups, le sujet de l'optimisation des performances Drupal a comme très souvent refait son apparition. En revanche, l'angle demandé était un peu moins habituel car assez spécifique. Comment optimiser les performances Drupal, donc, mais "par le cache" uniquement. Sujet assez dense et technique, mais qui est vraiment intéressant tant il est vaste et complexe.

Par anavarre

Bilan 2013 et perspectives 2014

L'année 2013 aura littéralement été une année en demi-teinte pour DrupalFacile. Les 6 premiers mois de l'année ont été intenses avec des vidéos postées régulièrement et même un record de traffic battu en mars (6924 visiteurs uniques). Hélas, depuis avant l'été, le site est au point mort et DrupalFacile reçoit régulièrement des encouragements et des demandes de vidéos qui sont précieusement consignées !

Par anavarre

Meetup Drupal Lyon - Présentation sur la sécurité

Pour le dernier meetup Drupal Lyon avant la coupure de l'été, j'ai fait une présentation sur la sécurité. Puisque le panel est assez hétérogène et qu'on parle aussi bien à des nouveaux venus sur Drupal qu'à des experts, je me suis dit qu'il serait intéressant de faire une présentation à la fois high-level et qui récapitule un ensemble de bonnes pratiques sur la sécurité qu'on a parfois tendance à oublier. L'idée principale c'est : ne pensez pas être "nul" en sécurité ou pas assez expert pour en parler ou vous y coller.

Pages