Creación de un filtro expuesto personalizado en Views

07/02/2017
creación filtro personalizado en views

Introducción: ¿Qué son los filtros?

Los filtros permiten definir condiciones para que se muestren unos elementos determinados en una vista. Por ejemplo, podemos añadir un filtro para que sólo se muestren los nodos de tipo Noticias, o sólo las noticias publicadas en el año 2014 (o ambos).

Los filtros pueden estar ocultos, con lo que se aplicarán siempre, o pueden estar expuestos al usuario (como veremos en este manual), de forma que sea el usuario quien seleccione qué filtros aplicar, convirtiendo la vista en una especie de buscador.

A continuación veremos cómo podemos añadir un filtro expuesto personalizado a nuestra vista.

Implementación del hook_views_api()

El primer paso es incluir en el fichero .module de nuestro módulo personalizado la implementación del hook_views_api(), indicándole qué versión de Views estamos usando y dónde alojaremos el archivo que manejará la declaración de handlers.

/**
 * Implements hook_views_api().
 */
function MY_MODULE_views_api() { 
  return array( 
    'api' => '3.0', 
    'path' => drupal_get_path('module', 'MY MODULE') . '/includes', 
  ); 
}
 
Hay que tener en cuenta si nuestra funcionalidad está dentro de una feature. En tal caso, si en dicha feature se ha añadido con anterioridad alguna vista, ya incluiría la implementación del hook_views_api(), por lo que no habría que añadirla al fichero .module, ya que generaría error por duplicidad de función.

Implementación del hook_views_data_alter() y hook_views_data()

El siguiente paso será indicarle a Views el nuevo filtro que queremos añadir. Para ello, creamos un archivo denominado 'MY_MODULE.views.inc' y lo guardamos dentro de la carpeta del módulo. Se recomienda crear una carpeta 'includes' donde guardar estos archivos para una mejor organización. En este punto podemos encontrarnos con dos escenarios:

  1. En caso de que queramos añadir información a una tabla ya conocida por Views: implementaremos dentro de este archivo el 'hook_views_data_alter'.
/** 
 * Implements hook_views_data_alter(). 
 */ 
function MY_MODULE_views_data_alter(&$data) { 

  if ( isset($data['TABLE_NAME']) && !isset($data['TABLE_NAME']['FILTER_NAME']) ) { 

    $data['TABLE_NAME']['FILTER_NAME'] = array( 
  	  'title' => t('Example title'), 
  	  'group' => t('Example group'), 
  	  'help' => t(''Example description.'), 
  	  'filter' => array( 
  	    'handler' => 'MY_MODULE_handler_filter_MY_FILTER', 
  	  ), 
  	); 
  } 
}

Ejemplo práctico: Imaginemos que contamos con un módulo personalizado denominado 'solicitud_de_verificacion' y deseamos añadir un filtro que cuente el número de palabras del título de un nodo (estaríamos añadiendo información a la tabla 'node' la cual ya es conocida por Views).

/** 
 * Implements hook_views_data_alter(). 
 */ 
function solicitud_de_verificacion_views_data_alter(&$data) { 
 
  if ( isset($data['node']) && !isset($data['node']['title_count']) ) { 

    $data['node']['title_count'] = array( 
  	  'title' => t('Title word count'), 
  	  'group' => t('My own group'), 
  	  'help' => t('Count the number of words in titles.'), 
  	  'filter' => array( 
  	    'handler' => 'solicitud_de_verificacion_handler_filter_field_count', 
  	  ), 
  	); 
  } 
}

2. Si estamos añadiendo tablas a través de nuestro módulo: entonces deberemos implementar el 'hook_views_data' para describir la nueva tabla.

Ejemplo hook_views_data().

/** 
 * Implements hook_views_data(). 
 */ 
function MY_MODULE_views_data() { 
  // This example describes how to write hook_views_data() for the following 
  // table: 
  // 
  // CREATE TABLE example_table ( 
  //   nid INT(11) NOT NULL         COMMENT 'Primary key; refers to {node}.nid.', 
  //   plain_text_field VARCHAR(32) COMMENT 'Just a plain text field.', 
  //   numeric_field INT(11)        COMMENT 'Just a numeric field.', 
  //   boolean_field INT(1)         COMMENT 'Just an on/off field.', 
  //   timestamp_field INT(8)       COMMENT 'Just a timestamp field.', 
  //   PRIMARY KEY(nid) 
  // ); 


  // The 'group' index will be used as a prefix in the UI for any of this 
  // table's fields, sort criteria, etc. so it's easy to tell where they came 
  // from. 
  $data['example_table']['table']['group'] = t('Example table'); 

  // Define this as a base table – a table that can be described in itself by 
  // views (and not just being brought in as a relationship). In reality this 
  // is not very useful for this table, as it isn't really a distinct object of 
  // its own, but it makes a good example. 
  $data['example_table']['table']['base'] = array( 
    'field' => 'nid', // This is the identifier field for the view. 
    'title' => t('Example table'), 
    'help' => t('Example table contains example content and can be related to nodes.'), 
    'weight' => -10, 
  ); 

  // This table references the {node} table. The declaration below creates an 
  // 'implicit' relationship to the node table, so that when 'node' is the base 
  // table, the fields are automatically available. 
  $data['example_table']['table']['join'] = array( 
    // Index this array by the table name to which this table refers. 
    // 'left_field' is the primary key in the referenced table. 
 // 'field' is the foreign key in this table. 
    'node' => array( 
      'left_field' => 'nid', 
      'field' => 'nid', 
    ), 
  ); 

  // Node ID table field. 
  $data['example_table']['nid'] = array( 
    'title' => t('Example content'), 
    'help' => t('Some example content that references a node.'), 
    // Define a relationship to the {node} table, so example_table views can 
    // add a relationship to nodes. If you want to define a relationship the 
    // other direction, use hook_views_data_alter(), or use the 'implicit' join 
    // method described above. 
    'relationship' => array( 
      'base' => 'node', // The name of the table to join with. 
      'base field' => 'nid', // The name of the field on the joined table. 
      // 'field' => 'nid' -- see hook_views_data_alter(); not needed here. 
      'handler' => 'views_handler_relationship', 
      'label' => t('Default label for the relationship'), 
      'title' => t('Title shown when adding the relationship'), 
      'help' => t('More information on this relationship'), 
    ), 
  ); 

  // Example plain text field. 
  $data['example_table']['plain_text_field'] = array( 
    'title' => t('Plain text field'), 
    'help' => t('Just a plain text field.'), 
    'field' => array( 
      'handler' => 'views_handler_field', 
      'click sortable' => TRUE, // This is use by the table display plugin. 
    ), 
    'sort' => array( 
      'handler' => 'views_handler_sort', 
    ), 
    'filter' => array( 
      'handler' => 'views_handler_filter_string', 
    ), 
    'argument' => array( 
      'handler' => 'views_handler_argument_string', 
    ), 
  ); 

  // Example numeric text field. 
  $data['example_table']['numeric_field'] = array( 
    'title' => t('Numeric field'), 
    'help' => t('Just a numeric field.'), 
    'field' => array( 
      'handler' => 'views_handler_field_numeric', 
      'click sortable' => TRUE, 
    ), 
    'filter' => array( 
      'handler' => 'views_handler_filter_numeric', 
    ), 
    'sort' => array( 
      'handler' => 'views_handler_sort', 
    ), 
  ); 

  // Example boolean field. 
  $data['example_table']['boolean_field'] = array( 
    'title' => t('Boolean field'), 
    'help' => t('Just an on/off field.'), 
    'field' => array( 
      'handler' => 'views_handler_field_boolean', 
      'click sortable' => TRUE, 
    ), 
    'filter' => array( 
      'handler' => 'views_handler_filter_boolean_operator', 
      // Note that you can override the field-wide label: 
      'label' => t('Published'), 
      // This setting is used by the boolean filter handler, as possible option. 
      'type' => 'yes-no', 
      // use boolean_field = 1 instead of boolean_field <> 0 in WHERE statement. 
      'use equal' => TRUE, 
    ), 
    'sort' => array( 
      'handler' => 'views_handler_sort', 
    ), 
  ); 

  // Example timestamp field. 
  $data['example_table']['timestamp_field'] = array( 
    'title' => t('Timestamp field'), 
    'help' => t('Just a timestamp field.'), 
    'field' => array( 
      'handler' => 'views_handler_field_date', 
      'click sortable' => TRUE, 
    ), 
    'sort' => array( 
      'handler' => 'views_handler_sort_date', 
    ), 
    'filter' => array( 
      'handler' => 'views_handler_filter_date', 
    ), 
  ); 

  return $data; 
}

En este punto ya podríamos ver los filtros que hemos creado en el listado de Views.

Ejemplo creado con la implementación que hemos hecho de hook_views_data_alter():

Ejemplo creado con la implementación que hemos hecho de hook_views_data():

Configuración del .info

A continuación debemos añadir la información sobre el fichero que contiene el handler al archivo .info del módulo. Añadimos la siguiente línea y limpiamos caché.

files[] = includes/MY_MODULE_handler_filter_MY_FILTER.inc

Creación del archivo my_module_handler_filter_my_filter.inc

Por útimo creamos el archivo en cuestión que contendrá el handler del filtro y lo situamos dentro de la carpeta correspondiente. La clase de la que extienda dependerá de la funcionalidad que queramos para nuestro filtro. Siguiendo el ejemplo anterior del contador de palabras del título de un nodo, haríamos uso de la clase 'views_handler_filter_numeric' que nos provee los métodos necesarios para ello. Estos métodos que nos ofrece la clase podemos sobreescribirlos a nuestras necesidades en caso de que queramos que tengan un comportamiento diferente para nuestro filtro. En el siguiente enlace podremos consultar en la API de Drupal todas las clases disponibles para nuestros filtros.

https://api.drupal.org/api/views/handlers!views_handler_filter.inc/group/views_filter_handlers/7.x-3.x

La clase base para los filtros sería 'views_handler_filter'. Otras clases interesantes y que hemos visto en el ejemplo del hook_views_data() son:

- Para campos de texto: views_handler_filter_string

- Para campos numéricos: views_handler_filter_numeric

- Para campos booleanos: views_handler_filter_boolean_operator

- Para campos de fecha: views_handler_filter_date

Ejemplo de nuestro handler para el contador de palabras del título de un nodo:

 
class solicitud_de_verificacion_handler_filter_field_count extends views_handler_filter_numeric { 
  function operators() { 
    $operators = parent::operators(); 
    // We won't be using regex in our example 
    unset($operators['regular_expression']); 
 
    return $operators; 
  } 
 
  // Helper function to return a sql expression 
  // for counting words in a field. 
  function field_count() { 
    // Set the real field to the title of the node 
    $this->real_field = 'title'; 
 
    $field = "$this->table_alias.$this->real_field"; 
    return "LENGTH($field)-LENGTH(REPLACE($field,' ',''))+1"; 
  } 
 
  // Override the op_between function 
  // adding our field count function as parameter 
  function op_between($field) { 
    $field_count = $this->field_count(); 
 
    $min = $this->value['min']; 
    $max = $this->value['max']; 
 
    if ($this->operator == 'between') { 
      $this->query->add_where_expression($this->options['group'], "$field_count BETWEEN $min AND $max"); 
    } 
    else { 
      $this->query->add_where_expression($this->options['group'], "($field_count <= $min) OR ($field_count >= $max)"); 
    } 
  } 
 
  // Override the op_simple function 
  // adding our field count function as parameter 
  function op_simple($field) { 
    $field_count = $this->field_count(); 
 
    $value = $this->value['value']; 
 
    $this->query->add_where_expression($this->options['group'], "$field_count $this->operator $value"); 
  } 
}

En nuestro ejemplo hemos realizado los siguientes pasos: 

1. Hemos creado nuestra propia versión del método operators() tomándolo de la clase padre:

function operators() {

   $operators = parent::operators();

2. Hemos eliminado la comparación por expresión regular: 

unset($operators['regular_expression']); 

ya que en nuestro filtro no vamos a hacer uso de las expresiones regulares.

3. Hemos implementado una función de ayuda que usa una combinación entre las funciones 'length' y 'replace' de mysql:

function field_count() {   

   $this->real_field = 'title';

   $field = "$this->table_alias.$this->real_field";   

        return "LENGTH($field)-LENGTH(REPLACE($field,' ',''))+1";

 }

4. Hemos sobreescrito los métodos op_between y op_simple de la clase padre.

Necesitamos convertir el título del nodo en un valor numérico y añadir funciones adicionales de mysql (length y replace) en el proceso. Para ello, filtraremos haciendo uso del método “add_where_expression”, el cual nos permitirá crear expresiones más complejas. 

De este modo, las funciones mysql que hemos añadido en nuestra función auxiliar “field_count()” pueden ser evaluadas también.

function op_between($field) {

   $field_count = $this->field_count();

   $min = $this->value['min'];

   $max = $this->value['max'];

   if ($this->operator == 'between') {

     $this->query->add_where_expression($this->options['group'], "$field_count BETWEEN $min AND $max");

   }

  }

Con esto, ya tenemos implementado nuestro handler para el contador de palabras del título de un nodo. Si accedemos a la configuración del filtro veríamos lo siguiente:
 
 

Y el resultado de la vista quedaría como se muestra a continuación:

Devolviéndonos los nodos cuyo título tiene el número de palabras indicado en el filtro expuesto.