Theming de un módulo propio en drupal 6

Imagen de Alessandro Mascherpa

Aunque se pueden encontrar módulos que aporten únicamente funcionalidad que se use desde otros módulos, y por lo tanto no cuenten con hooks propios de Drupal, lo más habitual es que contengan hook_menu. Este es un buen sitio por donde empezar a construir un módulo. En este caso:

<?php
/**
* Implementation of hook_menu().
*
* Define un enlace para visitar la página donde se muestran los
* datos estructurados con las funciones theme.
*/
function mimodulo_menu() {
 
$items = array();

 

$items['mimodulo-bad'] = array(
   
'title' => 'D6 bad demo',
   
'description' => 'D6 bad demo',
   
'page callback' => 'show_data_bad',
   
'access callback' => TRUE,
   
'menu_name' => 'primary-links',
  );

  return

$items;
}
?>

En el hook_menu se define la función callback que se encargara de responder cuando se acceda a la ruta definida como clave del array asociativo que define los elementos de menú. Esta función será la encargada de reunir y procesar (si fuese necesario) los datos y darles formato para mostrarlos al usuario. La implementamos de la siguiente manera:

<?php
/**
* Función Callback que se encarga de mostrar la página con los
* datos estructurados con la llamada desde menu.
*/
function show_data_bad() {
 
$data = mock_data(9);
 
$output = '';
 
 
$output = '<ul>';
 
  foreach (
$data as $item) {
   
$output .= '<li>';
   
   
$output .= '<strong>' . t('Image') . '</strong>' . ': ' .
   
'<img src="' . $item['image_path'] . '" /><br />';
   
$output .= '<strong>' . t('Title') . '</strong>' . ': ' .
   
'<span class="mimodulo-title">' . $item['title'] . '</span><br />';
   
$output .= '<strong>' . t('Body') . '</strong>' . ': ' .
   
'<span class="mimodulo-body">' . $item['body'] . '</span>';
   
   
$output .= '</li>';
  }
 
 
$output .= '</ul>';
 
  return
$output;
}
?>

En este ejemplo hemos separado el origen de datos en una función a parte:

<?php
/**
* Función que devuelve datos ficticios para hacer pruebas
*
* @param int $index
*         Número de elementos a devolver
*
* @return array()
*       Cada item contiene [1] imágen, [2] título y [3] cuerpo
*/
function mock_data($index) {
  global
$base_url;
 
$data = array();

 

$dataUnit = array(
   
'image_path' => drupal_get_path('module', 'mimodulo') . '/images/200x150.png',
   
'title' => 'ramdom title sin sentido alguno',
   
'body' => 'ramdom body sin sentido alguno ramdom body sin sentido alguno'.
   
' ramdom body sin sentido alguno ramdom body sin sentido alguno ramdom body'.
   
' sin sentido alguno ramdom body sin sentido alguno ramdom body sin sentido'.
   
' alguno ramdom body sin sentido alguno ramdom body sin sentido alguno '.
   
'ramdom body sin sentido alguno',
  );

  for (

$i = 0; $i < $index; $i++) {
   
$data[] = $dataUnit;
  }

  return

$data;
}
?>

En este punto ya tendríamos el objetivo cumplido. Mostramos un enlace en menu a una página que se encarga de extraer datos de un origen, puede tratarse de una base de datos, un servicio web, etc., y da formato a estos datos con html para mostrarlos al usuario de manera adecuada. Sin embargo la solución no es facilmente reutilizable. Esto se debe a que la estructura que se le da a los datos esta dentro de la función que responde a la llamada del usuario y por lo tanto para modificarla (adaptarla para su uso en otro sistema) habría que "hackear" el módulo, lo cual no nos interesa en absoluto. Drupal soluciona este problema a través del theme system como veremos a continuación.

Mejoramos la capacidad de extensión o modificación del módulo haciendo uso del theme system de Drupal

El theme system de Drupal nos permite definir funciones y plantillas en nuestros módulos de manera que se pueda conseguir esta separación entre datos y presentación y que a demás dichas funciones y plantillas se puedan sobreescribir en diferentes puntos del sistema para modificarlas sin necesidad de "hackear" los originales. Principalmente esta sobreescritura se realizará en el tema de nuestro sitio web. El theme system se puede usar de diferentes maneras.

  1. Uso de las funciones theme de la api

    Lo primero que deberíamos hacer para mejorar la extensibilidad de la capa de presentación de nuestro módulo será investigar si entre las funciones disponibles en la api de Drupal, que son bastantes, existe alguna que cumpla con nuestras necesidades actuales, ya que todas ellas se pueden modificar en el tema.

    A modo de ejemplo podríamos usar la función theme_image de la siguiente manera:

    <?php
    // En lugar de:
    $output .= '<img src="' . $item['image_path'] . '" />';

    // Podemos usar:
    $output .= theme("image", $item['image_path']);
    ?>

    El resultado será el siguiente:

    <img src="/sites/all/modules/custom/mimodulo/images/200x150.png" alt="" title="" width="200" height="150">

    Que entre otras bondades nos ha insertado automáticamente la altura y anchura de la imagen para mejorar el tiempo de renderizado en el navegador.

    En este punto hay que hacer notar que, aunque la especificación de la función theme sea:

    <?php
    theme_image
    ($path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE)
    ?>

    la llamada se realiza a una función theme que contiene como primer parametro el nombre parcial de la función completa:

    <?php
    theme
    ('image', $path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE)
    ?>

    Esto ocurre con todas las llamadas a funciones de la capa de presentación.

  2. Implementar hook_theme, de manera que el sistema sea consciente de que existen las funciones theme.

    ¡IMPORTANTE!: Antes de probar si funciona o como quedan los cambios (si hemos hecho cambios en hook_theme o en las funciones) hay que vaciar cache. De esta manera se actualiza el registro de temas.

    Si ninguna de las funciones disponibles en la API nos sirve, o no nos interesa que dichas funciones se tengan que sobreescribir en el tema, ya que afectaría al resto del sistema, podemos implementar nuestras propias funciones.

    Para conseguirlo primero que nada hay que implementar el hook_theme. En el especificaremos el nombre de la función tal y como aparecerá en la llamada a la función theme, sin el "theme_" inicial. Tambien especificaremos los argumentos de la función. El orden en el que aparezcan es importante, y debe ser el mismo que el de los parámetros de la implementación de la función, que si llevará el "theme_" inicial, como veremos en el siguiente punto.

    <?php
    /**
    * Implementation of hook_theme().
    *
    * Aquí se definen las funciones theme y si son template o no,
    * de manera que el sistema conozca su existencia.
    */
    function mimodulo_theme() {
     
    $themef = array();
     
     
    $themef['mimodulo_item'] = array(
         
    'arguments' => array($item=>NULL),
      );
     
      return
    $themef;
    }
    ?>

    Para conseguirlo primero que nada hay que implementar el hook_theme. En el especificaremos el nombre de la función tal y como aparecerá en la llamada a la función theme, sin el "theme_" inicial. Tambien especificaremos los argumentos de la función. El orden en el que aparezcan es importante, y debe ser el mismo que el de los parámetros de la implementación de la función, que si llevará el "theme_" inicial, como veremos en el siguiente punto.

  3. Implementar la función theme
    <?php
    /**
    * Renderiza un data item de mimodulo.
    *
    * @ingroup themeable
    */
    function theme_mimodulo_item($item) {
     
    $output = '';
     
      if ( isset(
    $item) ) {
       
    $output .= '<strong>' . t('Image') . '</strong>' . ': ' .
       
    theme("image", $item['image_path']) . '<br />';
       
    $output .= '<strong>' . t('Title') . '</strong>' . ': ' .
       
    '<span class="mimodulo-title">' . $item['title'] . '</span><br />';
       
    $output .= '<strong>' . t('Body') . '</strong>' . ': ' .
       
    '<span class="mimodulo-body">' . $item['body'] . '</span>';
      } else {
       
    drupal_set_message(t('Empty item in theme_mimodulo_item()'), $type = 'error', $repeat = FALSE);
      }
     
      return
    $output;
    }
    ?>

    Que ahora se llamará así en el callback del menú:

    <?php
    /**
    * Función Callback que se encarga de mostrar la página con los
    * datos estructurados con la llamada desde menu. Igual que la de
    * antes pero con llamadas a función theme para estructurar los datos
    */
    function show_data_good() {
     
    $data = mock_data(9);
     
      foreach (
    $data as $item) {
       
    $output .= '<li>';
       
    $output .= theme('mimodulo_item', $item);
       
    $output .= '</li>';
      }
     
      return
    '<ul>' . $output . '</ul>';
    }
    ?>

    De esta manera hemos conseguido separar el marcado de los datos a estructurar y podemos sobreescribir la función modificando el marcado para adaptarlo a otras necesidades, como veremos en el siguiente punto.

  4. Modificar la función en el tema

    En el caso de queramos sobreescribir un función en un tema podemos hacerlo, en el archivo template.php del tema, de dos maneras, cambiando el "theme_" de la implementación por:

    • "phptemplate_" + el nombre de la función definido en hook_theme
    • "nombredeltema_" + el nombre de la función definido en hook_theme. En el caso del ejemplo "garland_"

    Cuando llamemos a la función con theme() está buscará primero las que tiene registradas en el tema con "nombredeltema_", luego "phptemplate_" y por último "theme_".

    <?php
    /**
    * Renderiza un data item de mimodulo.
    *
    * @ingroup themeable
    */
    function garland_mimodulo_item($item) {
     
    $output = '';
     
      if ( isset(
    $item) ) {
       
    $output .= '<div class="mimodulo-image-label">' .
       
    t('Image') . ': ' . '</div>' .
       
    '<div class="mimodulo-image-field">' .
       
    theme("image", $item['image_path']) . '</div>';
       
       
    $output .= '<div class="mimodulo-title-label">' .
       
    t('Title') . ': ' . '</div>' .
       
    '<div class="mimodulo-title-field">' .
       
    $item['title'] . '</div>';
       
       
    $output .= '<div class="mimodulo-body-label">' .
       
    t('Body') . ': ' . '</div>' .
       
    '<div class="mimodulo-body-field">' .
       
    $item['body'] . '</div>';
      } else {
       
    drupal_set_message(t('Empty item in theme_mimodulo_item()'), $type = 'error', $repeat = FALSE);
      }
     
      return
    '<div class="mimodulo-item">' . $output . '</div>';
    }
    ?>
  5. Implementar la función con plantilla tpl.php

    Con Drupal también se puede implementar una función theme con una plantilla ".tpl.php", en lugar de con una función "theme_". Los pasos a seguir en este caso son:

    1. Actualizar el hook_theme para incluir la nueva función

      Incorporamos un nuevo item al array que devuelve el hook_theme.

      <?php
        $themef
      ['mimodulo_view'] = array(
           
      'arguments' => array('data' => NULL),
         
      'template' => 'mimodulo-view',
         
      'path' => drupal_get_path('module', 'mimodulo') . '/templates',
        );
      ?>
    2. Implementar las funciones preprocess

      La función preprocess se encarga de procesar los datos antes de cargarlos en la plantilla, de manera que en esta no haya que ejecutar lógica más alla de bucles, condicionales y impresión de los valores de las variables.

      <?php
      /**
      * Process variables for mimodulo-view.tpl.php.
      *
      * The $variables array contains the following arguments:
      * - $data
      *
      * @see mimodulo-view.tpl.php
      */
      function template_preprocess_mimodulo_view(&$variables) {
       
      $variables['items'] = array();
        foreach (
      $variables['data'] as $item) {
         
      $variables['items'][] = theme('mimodulo_item', $item);
        }
      }
      ?>
    3. Implementar la plantilla
      <?php
      /**
      * @file mimodulo-view.tpl.php
      * Renderiza una lista de items.
      *
      * - $items : Ya renderizados en preprocess.
      *
      * @see mimodulo_preprocess_mimodulo_view()
      */
      ?>

      <div class="mimodulo-items-list">
        <ul>
          <?php
         
      foreach ($items as $item): ?>

            <li class="mimodulo-list-item"><?php print $item; ?></li>
          <?php endforeach; ?>
        </ul>
      </div>

    La nueva función callback se verá así ahora.

    <?php
    function show_data_better() {
     
    $data mock_data(11);
      return
    theme('mimodulo_view', $data);
    }
    ?>
  6. Modificar la plantilla en el tema

    Si quisieramos que en un tema concreto los datos se visualizasen de otra manera, por ejemplo en forma de grid, habrá que repetir el último paso anterior, y posiblemente el penúltimo también. En nuestro casos repetiremos los dos reescribiendo la plantilla, que guardaremos en la carpeta del nuevo tema y la función preprocess que incluiremos en el archivo template.php del nuevo tema donde se quiera rediseñar la presentación de los datos:

    <?php
    /**
    * @file mimodulo-view.tpl.php
    * Renderiza una lista de items.
    *
    * - $items : Ya renderizados en preprocess.
    * - $gridnum : Num de celdas del grid.
    * - $length : total de items.
    * - $rows : total de filas.
    *
    * @see mimodulo_preprocess_mimodulo_view()
    */
    ?>

    <div class="mimodulo-items-list">
      <table>
        <?php for ($i = 0; $i < $rows; $i++):?>
        <tr>
          <?php for ($j = 0; $j < $gridnum; $j++):?>
            <td class="mimodulo-list-item">
            <?php if ( ($i*$gridnum+$j) < $length ) {print $items[$i*$gridnum+$j];} ?>
            </td>
          <?php endfor; ?>
        </tr>
        <?php endfor; ?>
      </table>
    </div>

    <?php
    /**
    * Process variables for mimodulo-view.tpl.php.
    *
    * The $variables array contains the following arguments:
    * - $data
    *
    * @see mimodulo-view.tpl.php
    */
    function bluemarine_preprocess_mimodulo_view(&$variables) {
     
    $variables['items'] = array();
     
    $variables['gridnum'] = 4;
     
    $variables['length'] = 0;
     
      foreach (
    $variables['data'] as $item) {
       
    $variables['items'][] = theme('mimodulo_item', $item);
       
    $variables['length']++;
      }
     
     
    $round = floor($variables['length']/$variables['gridnum']);
     
    $variables['rows'] = ($variables['length']%$variables['gridnum'])>0 ? $round+1 : $round;
    }
    ?>

    La manera de nombrar las funciones preprocess y el orden de ejecución que siguen se puede encontrar aquí

Funciones para filtrar la salida de datos

Algo que es importante mencionar es que se deberían "limpiar" siempre los datos que se muestran al usuario. Esto se consigue, principalmente, con 3 funciones alternativas:

  • check_plain: Comprueba que el contenido que se le pasa contenga únicamente texto plano.
  • check_markup : Más compleja, permite filtrar de manera selectiva el contenido con los filtros definidos por Drupal de manera que la salida sea texto con marcado HTML que siga las especificaciones del filtro.
  • filter_xss: Protege contra ataques XSS (crosScripting). Útil con el contenido que entra desde fuentes no controladas.

Generalmente se usan en las funciones theme o en las preprocess.

Añadir archivos CSS y JS específicos del módulo

Aunque se pueden añadir estilos CSS o comportamientos JS en el tema, puede que necesitemos incluir algunos por defecto. Esto se puede llevar a cabo, a demas de escribiendo los archivos CSS y JS con los contenidos adecuados e incluyendolos en el directorio del módulo incluyendolos en el código cuando vayan a resultar necesarios. Esto se hace con dos funciones de la API de Drupal:

  • drupal_add_js permite incluir archivos JS de nuestro módulo de la siguiente manera: drupal_add_js(drupal_get_path('module', 'mimodulo') . '/miarchivo.js');
  • drupal_add_css de la misma manera permite incluir archivos CSS de nuestro módulo de la siguiente manera: drupal_add_css(drupal_get_path('module', 'mimodulo') . '/miarchivo.css');

Estos archivos se incluirán en la cabecera antes que los del tema y por lo tanto los estilos tendrán menos pesos que si los reescribimos en las hojas de estilo del tema.