Planète

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']) && $_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

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 :

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

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 :

<?php

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

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").

    <description>
      <![CDATA[ <?php print html_entity_decode($description); ?> ]]>
    </description>

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 < $length; $i++)
    {
        $current = ord($value{$i});
        if (($current == 0x9) ||
            ($current == 0xA) ||
            ($current == 0xD) ||
            (($current >= 0x20) && ($current <= 0xD7FF)) ||
            (($current >= 0xE000) && ($current <= 0xFFFD)) ||
            (($current >= 0x10000) && ($current <= 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 :

<?php

/**
 * @file
 * Default view template to display a item in an RSS feed.
 *
 * @ingroup views_templates
 */

?>
  <item>
    <title><?php print montheme_xmlcleaner($title); ?></title>
    <link><?php print $link; ?></link>
    <description>
      <![CDATA[ <?php print montheme_xmlcleaner(html_entity_decode($description)); ?> ]]>
    </description>
    <?php print $item_elements; ?>
  </item>

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

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

<?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' =>  array(
    // 'title' => t('Administer my module'),
    // 'description' => t('Perform administration tasks for my module.'),
    // ),
    'access multi-steps forms' => array(
      'title' => t('Access multi-steps forms'),
      'description' => t('Enable the user to fill-in custom multi-steps forms'),
    ),
  );
}

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

  $items['myform'] = array(
    'title' => t('My multi-steps form'),
    'page callback' => 'multi_etapes_myform_page',
    'access arguments' => array('access multi-steps forms'),
    'type' => MENU_NORMAL_ITEM,
    'file' => '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' => array('value' => t('Bio'), 'weight' => -103),
    '_myform_step2' => array('value' => t('Hobbies'), 'weight' => -102),
    '_myform_step3' => array('value' => t('Summary'), 'weight' => -101)
  ));
  // Ajaxify the form stepping process.
  $controls->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

<?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->name)?$user->name:'Guest';
  
  // Create parameters to be passed to the multi-step form
  $params = array('uname' => $uname);

  // Return Drupal renderable array.
  return array(
    'mform' => 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, &$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, &$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, &$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(&$form_state, &$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' => 'textfield',
    '#disabled'=>true,
    '#title' => t('Login'),
    '#default_value' => isset($vals['loginv']) ? $vals['loginv'] : $params['uname'], // get login name from previous page (not submited but for design purpose).
  );

  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#default_value' => isset($vals['name']) ? $vals['name'] : NULL,
  );
  $form['email'] = array(
    '#type' => 'textfield',
    '#title' => t('Email'),
    '#default_value' => isset($vals['email']) ? $vals['email'] : NULL,
    '#required' => TRUE,
  );
  $form['www'] = array(
    '#type' => 'textfield',
    '#title' => t('Your web site'),
    '#default_value' => isset($vals['www']) ? $vals['www'] : NULL,
  );

  // store the login in a hidden field so that the value is submited

  $form['loginv'] = array(
    '#type' => 'hidden',
    '#default_value' => 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, &$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(&$form_state, &$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' => 'textarea',
    '#title' => t('Movies'),
    '#default_value' => isset($vals['movies']) ? $vals['movies'] : NULL,
  );
  $form['books'] = array(
    '#type' => 'textarea',
    '#title' => t('Books'),
    '#default_value' => isset($vals['books']) ? $vals['books'] : NULL,
  );
  $form['sports'] = array(
    '#type' => 'textarea',
    '#title' => t('Sports'),
    '#default_value' => 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(&$form_state, &$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' => 'fieldset',
    '#title' => t('Summary'),
  );

  $form['summary']['sum_bio'] = array(
    '#markup'=>
    "<h3>".t('Bio')."</h3>
    <ul>
      <li><em>".t('Login')." :</em> ${vals1['loginv']}</li>
      <li><em>".t('Name')." :</em> ${vals1['name']}</li>
      <li><em>".t('e-mail')." :</em> ${vals1['email']}</li>
      <li><em>".t('Website')." :</em> ${vals1['www']}</li>
     </ul>",
  );

  $form['summary']['sum_hobbies'] = array(
    '#markup'=>
    "<h3>".t('Hobbies')."</h3>
    <ul>
      <li><em>".t('Movies')." :</em> ${vals2['movies']}</li>
      <li><em>".t('Books')." :</em> ${vals2['books']}</li>
      <li><em>".t('Sports')." :</em> ${vals2['sports']}</li>
     </ul>",
  );



  $data = mforms_get_vals('myform');

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

  $form['message_team'] = array(
    '#type' => 'textarea',
    '#title' => t('A message for the team'),
    '#default_value' => isset($vals['message_team']) ? $vals['message_team'] : NULL,
  );

  $form['confirm']=array(
    '#type' => 'checkbox',
    '#title' => 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, &$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, &$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'] && !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

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 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

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

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

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

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.

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

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.

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é.

Thème 
Fil d'ariane
Modules
Drupal 7

Par Artusamak
Julien Dubois

Drupal et Scrum depuis les tranchées - Préparer la sprint définition

Drupal et Scrum depuis les tranchées - Préparer la sprint définition

Nous avons vu comment se déroule la sprint définition, voici maintenant quelques conseils pour faire en sorte que votre sprint définition se passe encore mieux grâce à de la préparation. Par ici pour les gens pressés.

Le succès d'une bonne sprint définition réside dans la qualité des stories qui constituent le backlog du produit. Les critères qui définissent une bonne story sont : sa taille (volume implicite de développement / configuration), la liste de ses critères d'acceptation (ils sont précis et facilement reproductibles), sa priorité (est-ce indispensable, bien d'avoir ou facultatif).
Réfléchir à ces éléments va donc prendre du temps, lorsque vous allez accompagner votre client, au tout début de la dernière semaine d'un sprint faites le point avec lui sur l'état de l'art de la priorisation et du niveau de définition des stories éligibles pour le prochaine sprint.
N'oubliez pas que votre client n'est pas un développeur web (sauf accident industriel), il ne sait pas ce qu'un module contrib peut faire clé en main. Vous avez un rôle de conseil à jouer pour l'aider à identifier ce qu'est une bonne taille de story et les bons critères d'acceptation qui l'accompagnent. L'une des clés à lui transmettre c'est de bien comprendre comment vous permettre d'itérer en se concentrant sur le découpage des fonctionalités en sous-groupes indispensable / important / facultatif.

Exemple : "je souhaite implémenter une fonctionnalité de recherche à facettes" on va d'abord se concentrer sur les données à indexer, les données à afficher puis on montrera au client le rendu de base pour ensuite se mettre d'accord sur les facettes à ajuster, les comportements spécifiques de filtres, la mise en page etc.

On préfèrera multiplier les stories de taille intermédiaire plutôt que les grosses stories pour la simple et bonne raison que plus vous aurez de fonctionnalités de taille limitée, plus il sera simple de mesurer vos progrès, d'ajuster vos priorités et de coller à votre vélocité.

Identifier les dépendances entre les stories est également important, cela évite à l'équipe de devoir complètement retrier le backlog à cause d'interdépendances non identifiées. Orienter les sprints sur des thèmes fonctionnels facilitera cette organisation, il n'y a rien de pire que des blocs fonctionnels démarrés dans tous les sens mais où rien n'est terminé. Scrum vous encourage à terminer des choses quitte à y revenir plus tard quand les besoins l'imposent plutôt que d'être concentrés sur 15 ou 20 fonctionnalités. J'ajouterai également que le mieux est l'ennemi du bien, à vouloir trop bien faire on fini par ne jamais faire.

TL;DR

Si on récapitule ce dont il faut s'assurer pour bien préparer sa sprint définition :

  • Rappeler tôt au client qu'il faut préparer le backlog
  • Aider le client à dimensionner et découper les stories en petits morceaux
  • Identifier les dépendances
  • Donner un thème aux sprints
Par juliendubreuil
julien dubreuil
Depuis 2009 maintenant, je développe avec le CMS/CMF Drupal. J’étais à la recherche d’un framework capable de remplacer un projet et je n’ai jamais arrêté de m’en servir depuis ce moment.

Drupal Commerce, ajouter un pane custom dans votre checkout

Drupal Commerce creation d'un pane de checkout

Comme nous avons pu le voir dans l’article précédent, il est simple de configurer le checkout de Drupal Commerce de façon à créer l’expérience utilisateur que vous désirez.

Dans cet article nous allons voir comment avec du code, ajouter et modifier des panes. Pour rappel, les panes sont les éléments qui composent les différentes pages du checkout. On y retrouve par exemple, le pane de paiement, d’adresse ou encore de choix de solution de transport.

Histoire d’illustrer cela, je vous propose de créer un module Drupal afin d’ajouter un pane permettant à vos clients de choisir parmi plusieurs emballages cadeaux. Ce simple module est accessible sur Github pour plus de compréhension.

Déclaration de notre nouveau Pane

La première chose à faire consiste à déclarer au système notre nouveau pane en utilisant le

hook_commerce_checkout_pane_info()

sandbox.module

1
2
3
4
5
6
7
8
9
function sandbox_commerce_checkout_pane_info() {
  $panes['paper_gift'] = array(
    'title' => t('Paper gifts'),
    'base' => 'paper_gift_pane',
    'page' => 'checkout',
  );

  return $panes;
}

Notre pane sera alors reconnu par Drupal et disponible dans le checkout. Dès à présent, vous pouvez activer le module et voir le pane apparaitre sur la page d’administration du checkout.

Comme à la manière des hooks de Drupal, le code qui génère l’affichage des panes va faire de l’introspection dans le code, de façon à chercher toutes les fonctions qui commenceront par la “base” que vous avez définie dans le hook_commerce_checkout_pane_info().

Ainsi le code sera à la recherche des fonctions suivantes :

  • BASE_settings_form()
  • BASE_checkout_form()
  • BASE_checkout_form_validate()
  • BASE_checkout_form_submit()
  • BASE_review()

Implémentation du formulaire d’administration du Pane

L’intérêt du pane est d’être affiché sur une page du checkout, néanmoins vous pouvez créer un formulaire de configuration pour celui-ci. Pour y accéder, rendez-vous sur l’interface d’administration du checkout. Notez qu’il n’y a pas de table dans laquelle les informations seront stockées, il vous faudra utiliser les méthodes variable_get() et variable_set() pour sauvegarder vos réglages.

sandbox.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * Pane: settings form callback.
 */
function paper_gift_pane_settings_form($checkout_pane) {
  $form['paper_gifts'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Ship this item in a gift box'),
    '#default_value' => variable_get('paper_gifts', array()),
    '#options' => paper_gift_pane_default_paper_gifts(),
  );

  return $form;
}

/**
 * Defines a list of default paper gifts to use.
 */
function paper_gift_pane_default_paper_gifts() {
  return array(
    'boy' => t('Boys'),
    'girl' => t('Girls'),
    'christmas' => t('Christmas'),
    'birthday' => t('Birthday'),
    'none' => t('None'),
  );
}

Dans le code ci-dessus, j’ai créé un formulaire d’administration et ajouté un champ texte, permettant au webmaster de modifier le texte qui sera affiché à l’utilisateur.

Création du Pane

Un pane n’est ni plus, ni moins qu’un formulaire dans le formulaire de checkout. Comme à l’habitude, vous y retrouverez le form, le form_validate et le form_submit.

Form

Le form contiendra ce qui sera affiché à l’utilisateur. Dans le cas de notre exemple, l’utilisateur pourra choisir entre plusieurs papiers cadeaux ou pas d’emballage.

sandbox.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * Pane: form callback.
 */
function paper_gift_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
  $paper_gifts = array();
  $default_paper_gifts = paper_gift_pane_default_paper_gifts();

  foreach ($default_paper_gifts as $name => $title) {
    $image = array(
      'path' => drupal_get_path('module', 'sandbox') . '/images/' . $name . '.jpg',
      'title' => $title,
      'alt' => $title,
    );
    $paper_gifts[$name] = theme('image', $image);
  }

  $pane_form['paper_gift'] = array(
    '#type' => 'radios',
    '#default_value' => 'none',
    '#title' => t('Select your paper gift'),
    '#description' => t('Your items will be shipped in a gift box.'),
    '#options' => $paper_gifts,
    '#attributes' => array('class' => array('paper-gift-pane-selection')),
  );

  $pane_form['paper_gift']['#attached']['css'] = array(
    drupal_get_path('module', 'sandbox') . '/css/sandbox.css',
  );

  return $pane_form;
}

Form validate

Le form_validate permet d’effectuer des opérations de validation sur les champs saisis par l’utilisateur.

sandbox.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * Pane: form validation callback.
 */
function paper_gift_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order) {
  // Validate the given value or set to none if null.
  if (!empty($form_state['values'][$checkout_pane['pane_id']])) {
    $pane_values = $form_state['values'][$checkout_pane['pane_id']];

    if (empty($pane_values['paper_gift'])) {
      $pane_values['paper_gift'] = 'none';
    }
  }
  return TRUE;
}

Ici, on regarde simplement s’il y a une valeur, sinon on définit que l’utilisateur ne veut pas d’emballage.

Form submit

La sauvegarde des informations intervient dans le form_submit une fois que l’on a passé la validation.

sandbox.module

1
2
3
4
5
6
7
8
9
10
11
/**
 * Pane: form submission callback.
 */
function paper_gift_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
  if (!empty($form_state['values'][$checkout_pane['pane_id']])) {
    $pane_values = $form_state['values'][$checkout_pane['pane_id']];
    if (!empty($pane_values['paper_gift'])) {
      $order->data['paper_gift'] = $pane_values['paper_gift'];
    }
  }
}

Remarquez que vous avez accès à l’objet $order et que vous pouvez faire ce que vous en voulez. Pour l’exercice j’aurais pu créer un nouveau line item, mais pour faire simple le choix de l’utilisateur résidera comme simple information dans la commande.

Form review

Une fois l’information attachée à la commande, il ne nous reste plus qu’à la faire paraitre sur le pane de review afin que l’utilisateur puisse la voir.

Pour cela il suffit d’implémenter le form_review

sandbox.module

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * Pane: Review
 */
function paper_gift_pane_review($form, $form_state, $checkout_pane, $order) {
  if ($order->data['paper_gift'] && $order->data['paper_gift']  != 'none') {
    $content['paper_gift'] = array(
      '#type' => 'item',
      '#markup' => t('Items in your order will be wrapped with the %paper paper gift', array('%paper' => $order->data['paper_gift'])),
    );
    return drupal_render($content);
  }
}

Modification d’un Pane existant

Comme mentionné précédemment, les panes ne sont que des forms et comme tout form vous avez la possibilité de les alterer via un hook_form_alter() ou encore mieux un hook_form_ID_alter().

Dans le cas ou vous voudriez modifier les paramètres d’un pane, vous implémentez la fonction hook_commerce_checkout_pane_info_alter()

Le mot de la fin

Au final, ce n’était pas si compliqué que cela ? En plus du fait d’avoir maintenant un pane pour offrir un emballage papier cadeau à vos clients, vous savez maintenant qu’il y a 3 grandes étapes dans la vie d’un pane. Le formulaire de settings pour l’admin, le formulaire destiné aux clients et le formulaire de review. Je serais curieux de savoir pourquoi vous avez besoin d’ajouter un pane dans votre checkout…
Comme mentionné au début de ce post, le code est accéssible sur Github.

Par hellosct1

Magazine Programmez Spécial Drupal

Le magazine 'Programmez', de l'été (juillet/août) 2014, numéro 176 a publié un supplément sous la forme d'un cahier spécial sur le CMS Drupa.l

programmez176.jpg

Le sommaire couvre de nombreux sujets, sous différents regards. Ainsi, vous trouverez de nombreux articles utiles comme :

  • Silicon Valley
  • Les Actus
  • Développeur du mois
  • Dossier sur l'inforamtique quantique
  • Coding For Fun
  • dossier sur les mobiles
  • etc.

Pour ma part, j'ai signé l'article suivant :

Bien démarrer avec Drupal

Commencer avec un nouveau CMS n'est jamais évident. Cependant la version 7 de Drupal a été améliorée pour mieux appréhender les projets et répondre aux attentes du web dynamique. Avec l'approche de la version 8, il est impératif de ne plus commencer de nouveaux projets en Drupal 6, même si celui-ci est disponible. il est temps de passer à Drupal 7

Consulter le magazine Programmez 176 en ligne

Par vincent59
Vincent Liefooghe

Influence du paramétrage APC sur les performances Drupal

Avec une version PHP inférieure à 5.5 il est recommandé d'utiliser un cache d'OpCode. Ce cache permet d'améliorer les performances de PHP, en mettant en cache le code PHP une fois qu'il a été analysé.

L'un des caches les plus utilisés est APC. Son installation sur une distribution type Debian consiste simplement en une commande

apt-get install php-apc

Par défaut, la taille mémoire réservée est de 32 Mo.

Si cette taille n'est pas optimale, les performances peuvent être dégradées au lieu d'être améliorées. En effet, on a alors un phénomène de saturation du cache, de fragmentation, et le code doit passer par des phases de check / miss / insert.

Pour nos tests, nous réalisons une première séance de tirs avec l'outil siege, pendant 2 minutes, sur une liste de 200 urls (les contenus ont été générés par le module Devel generate).

Résultats sans APC

Transactions:		         770 hits
Availability:		      100.00 %
Elapsed time:		      119.15 secs
Data transferred:	        3.97 MB
Response time:		        3.81 secs
Transaction rate:	        6.46 trans/sec
Throughput:		        0.03 MB/sec
Concurrency:		       24.63
Successful transactions:         766
Failed transactions:	           0
Longest transaction:	        4.65
Shortest transaction:	        0.55

Typiquement, sans Cache d'OpCode et sans cache Drupal, les performances sont assez faibles.

Résultats avec 16 Mo

Le résultat de la commande siege est :

Transactions:		        2928 hits
Availability:		      100.00 %
Elapsed time:		      119.51 secs
Data transferred:	       14.98 MB
Response time:		        1.02 secs
Transaction rate:	       24.50 trans/sec
Throughput:		        0.13 MB/sec
Concurrency:		       24.89
Successful transactions:        2917
Failed transactions:	           0
Longest transaction:	        2.78
Shortest transaction:	        0.55

Sur cette VM de test, on obtient 24 transactions par secondes, sans cache drupal activé. On constate que la taille mémoire utilisée par le cache est de 15 Mo environ. Par rapport à la configuration sans le cache APC, on augmente les performances d'un rapport 4 environ.

La consommation mémoire APC est la suivante :

Résultats avec 12 Mo

Transactions:		         784 hits
Availability:		       99.87 %
Elapsed time:		      119.73 secs
Data transferred:	        3.99 MB
Response time:		        3.75 secs
Transaction rate:	        6.55 trans/sec
Throughput:		        0.03 MB/sec
Concurrency:		       24.58
Successful transactions:         778
Failed transactions:	           1
Longest transaction:	        4.42
Shortest transaction:	        0.58

Dans ce cas, le paramétrage du cache APC n'est pas correct. En effet, la taille du cache n'est pas suffisante pour stocker toutes les opérations.
Du coup, les performances chutent drastiquement (environ 1/4 des performances précédentes), et l'on a des performances identiques au fonctionnement sans le cache APC.

On constate que le cache APC est saturé, et que l'on voit des "cache full count", qui signifient que le cache a été rempli et qu'il a fallu vider des éléments, ce qui est très pénalisant.

Résultats avec 24 Mo

Transactions:		        3104 hits
Availability:		      100.00 %
Elapsed time:		      119.67 secs
Data transferred:	       15.90 MB
Response time:		        0.96 secs
Transaction rate:	       25.94 trans/sec
Throughput:		        0.13 MB/sec
Concurrency:		       24.89
Successful transactions:        3085
Failed transactions:	           0
Longest transaction:	        1.53
Shortest transaction:	        0.48

Avec 24 Mo, les choses rentrent dans l'ordre. Le cache est correctement utilisé, mais il n'y a pas d'amélioration des performances.

Résultats avec 32 Mo

Transactions:		        2595 hits
Availability:		      100.00 %
Elapsed time:		      119.78 secs
Data transferred:	       13.48 MB
Response time:		        1.15 secs
Transaction rate:	       21.66 trans/sec
Throughput:		        0.11 MB/sec
Concurrency:		       24.87
Successful transactions:        2583
Failed transactions:	           0
Longest transaction:	        5.92
Shortest transaction:	        0.89

Conclusion

Cette série de tests a montré que le paramétrage du cache APC doit être régulièrement monitoré et validé, sous peine d'avoir un effet inverse à celui attendu et de dégrader les performances.

On peut également constater qu'il ne sert à rien de surallouer la mémoire, car une fois la taille optimale allouée, il n'y a aucun gain.

Cela prouve également que le choix de l'hébergeur est important, et que la richesse du CMS Drupal a également des impacts sur les performances de la plate-forme. Il convient donc de faire le bon choix. Un hébergement mutualisé ne pourra généralement pas offrir la souplesse d'un hébergeur spécialisé, capable de mettre en oeuvre les bons composants et les bons paramétrages.

Addendrum : impact du cache Drupal

Les tests ont été réalisés avec le cache désactivé. De ce fait, de nombreuses requêtes MySQL sont générées, et tendent à ralentir le débit.
Si on active le cache Drupal pour les utilisateurs anonymes, les résultats sont spectaculaires.

Un premier essai sans cache APC, mais avec le cache Drupal :

Transactions:		        3610 hits
Availability:		      100.00 %
Elapsed time:		      119.95 secs
Data transferred:	       18.70 MB
Response time:		        0.83 secs
Transaction rate:	       30.10 trans/sec
Throughput:		        0.16 MB/sec
Concurrency:		       24.91
Successful transactions:        3598
Failed transactions:	           0
Longest transaction:	        5.18
Shortest transaction:	        0.59

On constate une nette amélioration par rapport à un paramétrage sans cache (30 transactions / seconde vs 6.5). Au final, on est assez proche d'une configuration APC sans cache Drupal.

Deuxième essai avec le cache Drupal et le cache APC :

Transactions:		       23611 hits
Availability:		      100.00 %
Elapsed time:		      119.03 secs
Data transferred:	      122.00 MB
Response time:		        0.13 secs
Transaction rate:	      198.36 trans/sec
Throughput:		        1.02 MB/sec
Concurrency:		       24.93
Successful transactions:       23483
Failed transactions:	           0
Longest transaction:	        2.49
Shortest transaction:	        0.06

Lorsqu'on active les caches APC et Drupal, les performances sont multipliées par 30 !

N'oubliez donc pas, pour avoir de bonnes performances, d'activer tous les niveaux de cache possibles : au niveau Drupal, mais également au niveau PHP. Le choix d'un hébergeur qui supporte ces différents niveaux est un gage de performances. Mon hébergeur, HebInWeb.com, met en oeuvre APC sur les instances Drupal.

 

Catégorie: 


Tag: 

Par Artusamak
Julien Dubois

CMI : ... à la pratique

CMI : ... à la pratique

Nous avons précédemment vu la théorie à propos de CMI. C’est maintenant le moment de voir comment s’en servir dans des conditions réelles.

Afin de faire nos tests, je vous propose de monter deux Drupal 8 en parallèle. Appelons les Drupal-Prod et Drupal-Dev. Pour se faire, il suffit d’installer un Drupal 8 d’un côté, de prendre sa base de données et de l’injecter dans un second site.

Pour le moment, allons sur l’instance dite “dev” et faisons un petit changement rapide. Par exemple, allons changer le slogan du site.

Allez maintenant dans Configuration > Configuration Management > Onglet Full import/export > Export et cliquez sur le bouton export. Vous allez obtenir un fichier config.tar.gz

Gardez bien ce fichier au chaud, et allez faire un tour sur le 2ème site.

Sur la page Configuration > Configuration Management > Onglet Full import/export > Import, allez chercher votre config.tar.gz et uploadez le.

Si tout se passe bien, vous allez donc voir la page suivante vous indiquant qu’un élément a été modifié.

Cliquez sur le lien “View differences” et Drupal va vous montrer votre changement sous forme de “diff” directement.

Il suffit maintenant de valider les changements en soumettant le formulaire.
Et voilà, vos changements sont maintenant présents sur votre deuxième site.

Jusqu’ici, on aurait pu se contenter de transférer la base de données de “dev” sur “prod” et nous aurions eu le même résultat.
Néanmoins, n’oubliez pas une chose. Nous transférons ici uniquement de la configuration, et non du contenu. Cela signifie que le site de “prod” peut continuer à être alimenté en contenu, pendant que vous allez transférer la nouvelle configuration en production.

Utiliser CMI au sein d'une équipe

Le point important ici est: comment utiliser ce système afin de collaborer à plusieurs ?

Tout d’abord, il faut savoir que, par défaut, Drupal stocke les fichiers de configuration (les fameux fichiers « yaml ») dans un répertoire situé dans /sites/default/config/ avec une clé de hash. Cela doit ressembler à quelque chose du genre :

sites/default/files/config_1yrma6v3CrXoB35-RirGI9HNjvq1YBRNEHhezKiV6xdE6d4RiG6yrTu4el6LwKci1nuqjKtptA/staging

Néanmoins, et fort heureusement, ceci est facilement modifiable. En effet, cette clé est stockée dans votre settings.php, sous la forme :

$config_directories['staging'] = 'sites/default/files/config_1yrma6v3CrXoB35-RirGI9HNjvq1YBRNEHhezKiV6xdE6d4RiG6yrTu4el6LwKci1nuqjKtptA/staging’;

Il suffit donc de changer cette clé ici pour avoir quelque chose de plus « lisible », ou même de placer ce répertoire en dehors du webroot (bonne pratique de sécurité).

Pour notre test, changeons donc les deux clés du settings.php en :

$config_directories['active'] = 'sites/default/files/config_active';
$config_directories['staging'] = 'sites/default/files/config_staging';

Et maintenant, comment collaborer ?
C’est une chose relativement simple, à condition que chacun suive la même procédure.

Pour illustrer ceci, nous allons utiliser l’export/import de configuration via drush.

Les commandes importantes sont :

config-export (cex)   Export config from the active directory.
config-import (cim)   Import config from a config directory.

Pour commencer, lançons un export sur le site de production.

files git:(8.x) drush cex
The current contents of your export directory (sites/default/files/config_staging) will be deleted. (y/n): y
Configuration successfully exported to sites/default/files/config_staging.[success]

Puis commitez cette configuration initiale dans votre dépôt de code (git, svn …)

Chacun des développeurs va maintenant mettre à jour son dépôt local, puis importer la configuration.

A partir de ce moment, nous allons pouvoir chacun faire nos modifications en local, avant d’exporter à nouveau la configuration via drush, puis de commiter les changements.

L’agrégation des changements va se faire grâce à notre système de versionning de code.

Une fois la configuration d’une partie du site effectuée, la production sera alors mise à jour, la configuration à nouveau importée, et les changements seronts alors présent en production.
Tout simplement.

La solution repose donc, pour l’instant, sur un mélange entre ce qu’offre CMI et les fonctionnalités du gestionnaire de code (on ne le dira jamais assez, même si vous travaillez seul, c’est indispensable).

Personnellement, je ne pense pas que cela va beaucoup changer d’ici à la sortie de Drupal 8. Il va sûrement falloir se tourner vers la contrib afin de voir des solutions tierces, reposant sur ce qu’offre CMI, afin de réaliser ceci sans s’appuyer sur un gestionnaire de versionning de code.

Si vous souhaitez aller encore plus loin, nous ne pouvons que vous recommander la lecture de l'article produit par Nuvole : Packaging and reusing configuration in Drupal 8 [en]

Pages