Programar un tipo de campo en Drupal

Drupal es primeramente reconocido como un gestor de contenidos, en inglés se usan las siglas CMS. Instalado y ampliado con módulos se pueden contruir y configurar potentes sistemas de edición, por supuesto, pero Drupal es también visto como un potente entorno de programación, en inglés "framework". De hecho de los muchísimos módulos que hay para Drupal, una buena parte son módulos que hacen cosas a partir de simplemente usar y extender la funciones de su librería de funciones. Esta librería es tan amplia y sobre tantas áreas que se puede considerar usar Drupal como un sistema elegante de base, que sea el punto de partida con el que programar toda la web con módulos propios y totalmente a medida sin usar nada de la comunidad de módulos contribuidos.

Por muchos y muchos módulos que hayan en la comunidad de desarrollos contribuidos a Drupal siempre nos puede aparecer una necesidad nueva que no esté cubierta por ninguno de los que hay. Este fue el caso en el que nos encontramos recientemente. Queriamos un campo que nos permitiera definir listas de países en cualquier tipo de entidad de Drupal. Nos encontramos que no había ninguna que nos gustara la suficiente, porque los que hay son muy grandes y poco eficientes. Se nos ocurrió que estos módulos estaban dejando de lado la lista de países que ya lleva el core de Drupal, y de la que cada traducción oficial ya proporciona su traducción al idioma que cada uno use para construir su web.

Imaginamos que sería fácil construir un módulo que hiciera esto y empezamos a documentarnos sobre cómo crear este módulo. Hay muchas funciones para la API de campos y entidades y buscamos las necesarias para construir un tipo de campo. Como suele ser habitual la información está dispersa o es muy escueta. Nos guiamos por varios tutoriales que encontramos en una búsqueda rápida por Google, y aquí ofrecemos nuestro tutorial, que no solo cubre varias lagunas que nos hicieron dudar en los tutoriales que leímos, sinó que posiblemente sea el único que hay en español.

Para construir un módulo que aparezca como un campo hace falta abordar:

  • que éste aparezca en la lista de módulos de Drupal
  • que instale en la base de datos la estructura pertinente para guardar nuestro dato
  • que aparezca en cada tipo de contenido como un tipo de campo que se pueda activar y configurar
  • que aparezca posteriormente en el formulario de creación de un nodo u otro tipo de entidad con los datos que debemos guardar (en este caso como una lista de países para elegir uno)
  • que podamos definir en la configuración de la visualización del campo cómo se debe ver el dato guardado en la base de datos cuando aparece en la página de vista del nodo o entidad
  • que se verifique correctamente el dato antes de guardarlo en la base de datos para no tener errores de congruencia

Vamos a revisar el código para cada una estas partes.

Primeramente tenemos que escribir un fichero que permita a Drupal reconocer este módulo para que salga en la página de activación de módulo. Hay que construir un fichero "countries_list.info" dentro de una carpeta llamada "Countries list" y en ese fichero introducir el siguiente código.

name = Countries list
description = Provides a field for entities using the list of countries in core.
core = 7.x
version = 1.0.3
package = Fields
files[] = countries_list.module

Como vemos el código nos indica la versión y en que grupo de módulo debe aparecer: queremos que esté agrupado con todos los otros módulos de campos (como vemos en el texto resaltado en granate).

Ahora es el momento de crear la parte que hace que dispongamos de un campo para activar. Para ello, en la carpeta "Countries list" mencionada anteriormente crearemos también el fichero "countries_list.module". Este fichero es el verdadero contenedor de las funcionalidades.

/**
 * Implements hook_field_info().
 */
function countries_list_field_info() {
  return array(
    'countries_list_field' => array(
      'label' => t('Countries'),
      'description' => t('List of countries in core to be used as a field.'),
      'default_widget' => 'countries_list_field_widget',
      'default_formatter' => 'countries_list_field_formatter',
      'settings' => array(),
      'instance_settings' => array(),
    )
  );
}

Esta función es un "hook", una función general de Drupal extensible desde cada módulo. Vemos en negrita que al nombre de nuestro módulo "countries_list" le hemos añadido la coletilla "_field_info()" para definir la función. Todas las funciones de nuestro módulo deben empezar por "countries_list" y a continuación el nombre de la función "hook" que estamos extendiendo. En pantalla esto se ve así en la página en la que añadimos nuevos campos a nuestra entidad (tipo de contenido, usuario, comentario, pedido, o cualquier otra):

Tras crear el campo y colocarlo en la configuración del tipo de contenido habría que preveer que los datos que gestiona se guarden en la base de datos. Para ello hace falta un tercer fichero, con la definición del modelo de datos para la base de datos. En la misma carpeta creamos un fichero con el mismo nombre que los otros y la extensión "install".

/**
 * Implements hook_field_schema().
 */
function countries_list_field_schema($field) {
  $schema = array(
    'columns' => array(
      'country_code' => array(
        'type' => 'varchar',
        'not null' => FALSE,
        'length' => 11,

      ),
    ),
  );
  return $schema;
}

Como vemos en el anterior fragmento de código solo hace falta una función en este fichero. El texto marcado en granate nos indica qué nombre tendrá la columna donde se guardaran esos datos. Hay que preguntarse: ¿qué datos guardará este campo? Bien, este campo maneja una lista de países indexada. Por lo tanto guardará ese índice, que es el código internacional del país de dos letras. En la base de datos lo que veremos es una nueva tabla, con los campos normales más una columna nueva donde se guardan los índices de cada registro (cada nodo o entidad).

Esta tabla será creada automáticamente y el campo nuevo también será añadido automáticamente, no tenemos que preocuparnos de nada más. Tampoco hace falta crear ninguna función para guardar, actualizar o borrar datos o registros, de todo ello se encargan otras funciones internas de la API de campos. Debe tenerse en cuenta que si estoy haciendo un campo que guarda varios valores (un campo compuesto), hay que definir en la función "_field_schema()" todos los campos necesarios.

Ya tenemos el módulo, ya tenemos el campo en los formularios de creación de contenidos nuevos, y ya tenemos el dato almacenado en nuestra base de datos. Ahora hay que definir cómo se verá una vez que se presente la página del contenido del que forma parte.

Volvemos al fichero ".module" y seguimos ampliando con unas pocas funciones más. El "hook" llamado "_field_widget_form()" se encarga de mostrar ese campo en el formulario de creación del contenido. Si este campo está en un tipo de contenido esta función se encarga de mostrarlo en el formulario de "node/add/nuestro-tipo-de-contenido". Aquí hay un aspecto importante a programar: ¿qué tipo de campo hay que mostrar al usuario para que llene en los momentos de creación o de edición?

Como vemos en el siguiente fragmento de texto esta función se encarga de incorporar en el array del formulario un nuevo elemento de formulario de tipo "select" (ver elemento '#type'). En negrita hemos marcado qué se debe ver en la lista como '#options', que no es ni más ni menos que la lista de países del sistema. Esta lista es fácilmente accesible a través de la función de sistema "country_get_list()".

/**
 * Implements hook_field_widget_form().
 */
function countries_list_field_widget_form(&$form, &$form_state, $field, $instance, $lang, $items, $delta, $element) {
  switch ($instance['widget']['type']) {
    case 'countries_list_field_widget':
      // Display the form to let the user pick a country
      // country_get_list() is a core function on includes/locale.inc
      $options = country_get_list();
      $element['country_code'] = array(
        '#type' => 'select',
        '#title' => $element['#title'],
        '#options' => $options,
        '#required' => $element['#required'],
        '#empty_value' => 0,
        '#default_value' => isset($items[$delta]['country_code']) ? $items[$delta]['country_code'] : NULL,
      );
      break;
  }
  return $element;
}

Dentro del formulario del tipo de contenido esto se verá así, es decir como un menú desplegable con la lista de países del sistema perfectamente ordenada alfabéticamente en nuestro idioma, la principal razón que hace de nuestro módulo una buena elección:

Ya hemos seleccionado el país en la creación de un nodo o de un usuario, y al guardar iremos a su ficha de página. Ahora toca decirle al sistema cómo debe verse ese campo. Drupal usa la nomenclatura "formatter" para definir las funciones que se encargan de controlar la visualización del campo. Para ello escribiremos en nuestro fichero ".module" la función con el "hook" llamado "_field_formatter_info()".

Como vemos en el siguiente fragmento de código hemos marcado en negrita los 3 formateadores de esa información. Lo que queremos es que en la configuración de la visualización de ese campo el usuario pueda escoger entre esas 3 opciones que explicaremos en la siguiente función.

/**
 * Implements hook_field_formatter_info().
 */
function countries_list_field_formatter_info() {
  return array(
    'formatter_country_name' => array(
      'label' => t('Country name'),
      'field types' => array('countries_list_field')
    ),
    'formatter_country_code' => array(
      'label' => t('Country code'),
      'field types' => array('countries_list_field')
    ),
    'formatter_country_code_lowercase' => array(
      'label' => t('Country code lowercase'),
      'field types' => array('countries_list_field')
    ),
  );
}

En la página de configuración de la visualización del campo en su tipo de contenidos esto se muestra como un menú desplegable en el que sale el mismo texto que hemos puesto en los arrays de cada formatter como "label".

Ahora que ya podemos escoger entre las 3 opciones de visualización deben añadir un "hook" llamado "_formatter_view()" cuya misión es explicarle al sistema cómo se visualiza cada estilo. El anterior hook es solo una lista, en este llamamos a la lista en un switch y damos a cada caso su desarrollo.

El campo guarda solo el índice del país de dos letras, índice que ya nos devuelve la función core de Drupal de la lista de países. Este código internacional se muestra directamente si elejimos la opcion "Country code" que como vemos no hace nada más que mostrarlo (ver caso "formatter_country_code"). En la opción con "lowercase" simplemente lo convertimos en minúsculas por si en algún caso conviene.

Vale la pena observar como hacemos un foreach ($items as $delta => $item) para respetar el caso en el que el campo sea múltiple, sinó solo pintariamos el primer registro. También nos cubrimos las espaldas con el if ($country_code == '0') ya que esto nos protege de un error si visualizamos un nodo que fue creado antes de añadir este campo. Drupal nos permite añadir campos en cualquier momento, incluso podría hacer años que tuvieramos este tipo de contenido y hubieran cientos de nodos. Óbviamente todos ellos no tendrían valor en este campo todavía, si no hemos hecho algún tipo de edición sobre ellos.

/**
 * Implements hook_field_formatter_view().
 */
function countries_list_field_formatter_view($entity_type, $entity, $field, $instance, $lang, $items, $display) {
  $element = array();
  switch ($display['type']) {
    case 'formatter_country_name':
      foreach ($items as $delta => $item) {
        $country_code = $item['country_code'];
        if ($country_code == '0') {
          return;
        }
        $countries = country_get_list();
        $country_name = t($countries[$country_code]);
        $element[$delta]['#markup'] = $country_name;
      }
      break;
    
    case 'formatter_country_code':
      foreach ($items as $delta => $item) {
        $country_code = $item['country_code'];
        if ($country_code == '0') {
          return;
        }
        $element[$delta]['#markup'] = $country_code;
      }
      break;
    
    case 'formatter_country_code_lowercase':
      foreach ($items as $delta => $item) {
        $country_code = strtolower($item['country_code']);
        if ($country_code == '0') {
          return;
        }
        $element[$delta]['#markup'] = $country_code;
      }
      break;
  }
  return $element;
}

Un aspecto crítico son las funciones de validación de datos. Este es un punto muy poco documentado y que en muchos tutoriales se saltaban, y que en primera instancia me hicieron perder tiempo ya que el módulo no funcionaba. En algún lugar leí como un comentario, que era importante añadir estos dos hooks bastante estándares para cubrir cualquier error o incidencia.

function countries_list_widget_error($element, $error, $form, &$form_state) {
  switch ($error['error']) {
    case 'countries_list_field_invalid':
      form_error($element, $error['message']);
      break;
  }
}
/**
 * Implements hook_field_is_empty().
 */
function countries_list_field_is_empty($item, $field) {
  if (!empty($item['countries_list_field'])) {
    return true;
  }
}

Ya está, el módulo está completo. Para los que quieran descargarlo entero lo encontrarán en una sandbox que tengo en drupal.org: https://drupal.org/sandbox/jorditr/2189367.

No es un código muy largo, como se puede ver las funciones de la API de Drupal son tan potentes que hemos manejado unos pocos arrays, y le hemos indicado unas pocas líneas de código para hacer aquello que le es propio a nuestro módulo. A partir de aquí se pueden hacer muchos otros módulos muy similares a este. Por ejemplo, no hay ningún módulo que permita incrustar un documento de Slideshare solo con poner su URL. Sería muy fácil construir uno a partir de este código.

Si bien es cierto que hay que saber un poco de programación y que la API de Drupal es muy amplia y a veces compleja (lógicamente, estamos antes un framework muy potente, maduro y estructurado) tampoco hay que saber mucha programación PHP para crearnos un tipo de campo para nuestras necesidades concretas.

Extender la API de Drupal con nuevos elementos a veces es fácil y a veces es difícil, pero muchas veces es solo cuestión de encontrar la documentación adecuada. No siempre es fácil, hay tanta amplitud solo en la API de Drupal (ya no digamos si consideramos otros módulo contribuidos como Views) que lo más complejo es dar con la forma concreta de programar algo. Sirva este ejemplo para animarnos a todos a seguir ampliando Drupal con nuevas y apasionantes funcionalidades para nuestros proyectos.