Contextual content with the Flag module and a custom Views "Default Argument Handler"

The Views module is the most installed Drupal module and gives great power to developers for building websites. Because of its rich feature set, Views will be integrated in core as part of Drupal 8. Developers may extend the Views functionality by writing custom plugins using the complex plugin system. This helps implementing functionality for special use cases in Views.

In this blog post I would like to explain how to write a custom "Default Argument Handler" for Views and how to develop a simple context system using the Flag module by providing a sample use case as example.

Hint: The complete code of the module can be accessed for testing purposes at github.

The Use Case

Our client produces organic pet food as a family business and wants to increase sales using e-commerce. Furthermore, the daughter of our client will publish a monthly magazine with tips and tricks for each dogs and cats. The magazine should be advertised on the new website of the customer. Older magazine arcticles will be published on the website later on.

A key feature of the new website will be a context system which delivers products and articles to the user based on his favorite pet. We use the Flag module to flag the pets for each user.

Hint: To keep it simple we flag the pets by hand.

Preparations

Creating flags

First of all we create a vocabulary "flag context" with some terms which will be flagged later.

Afterwards we create a new flag at admin/structure/flags with the flag type "taxonomy term" choosing "flag context" as bundle. 

Extending the content type

By adding an "Entity Reference" field to a content type (i.e. article of the standard profile) we make sure that we can link content to the terms which will be flagged by the user. After adding the field we create at least one node for each term. For this use case we would also need to add this field to our products but this will be sufficient for our example.

Next, we have to flag a term for testing the functionality later. Just visit a term page and flag the term using the link on the page.

Creating Views

The user should see what their favorite pet is. We make this happen by creating a View.

The View filters terms by our created vocabulary "flag context" and has a relationship "Flags: Taxonomy Term flag" on "Flag Context" and  "Current User".  We use "block" as a display for easy embedding.

In addition to the title we also add a flag link allowing the user to unflag the term in the block.  

For testing purposes, we add the new block to the "sidebar first" region. 

At last, we create the basics for our final test. We create a page View showing only "article" nodes. We add "Content:has taxonomy term ID" as "Contextual Filter". This filter will be extended later with our context functionality.

Coding the handler

A views handler has to been included in a module to be recognized by the plugin system of Views. It is not a lot of work but despite that there are some lines where we could struggle. 

1. The .info file

A common pratice is to put views handlers and plugins in their own subdirectorys. Drupal needs to know where those files are located so we include the path into the info file.

  1. name = Views Flag Context
  2. description = Provides a flag context.
  3. core = 7.x
  4. package = Flags
  5. version = 7.x-0.1
  6.  
  7. dependencies[] = flag
  8.  
  9. files[] = handlers/views_argument_default_flag.inc

2. The .module file

We need to implement hook_views_api to tell Views that our module is of interest: 

  1. <?php
  2. /**
  3.  * Implements hook_views_api().
  4.  */
  5. function views_flag_context_views_api() {
  6.   return array(
  7.     'api' => '3',
  8.   );
  9. }
  10.  

Now, Views looks for a file with relevant code. Best practice is to create a modulename.views.inc file.

3. The .views.inc file

By implementing hook_views_plugin we return an array in which we define the kind of plugin we like to create and the place where to find it.

  1. <?php
  2.  /**
  3.   * Implements hook_views_plugins().
  4.   */
  5. function views_flag_context_views_plugins() {
  6.   $plugin = array(
  7.     'argument default' => array(
  8.       'views_argument_default_flag' => array(
  9.         'title' => t('Default Flags'),
  10.         'handler' => 'views_argument_default_flag',
  11.         'path' => drupal_get_path('module', 'views_flag_context') . '/handlers',
  12.       ),
  13.     ),
  14.   );
  15.   return $plugin;
  16. }
  17. ?>

With these 3 steps we prepared our handler. Now it is time to add functionality to the handler.

4. The views_argument_default_flag.inc file

In this file we define the handler as class by inheriting and overriding methods from the views_plugin_argument_default.

The tree:

  1. <?php
  2.  /**
  3.    * Define the options form to enable selection of flags.
  4.    */
  5.  
  6.   function options_form(&$form, &$form_state) {
  7.   }
  8.  
  9.   /**
  10.    * Return all possible options for the view and provide default values.
  11.    */
  12.   function option_definition() {
  13.   }
  14.   /**
  15.    * Provide the default form form for submitting options.
  16.    */
  17.   function options_submit(&$form, &$form_state, &$options = array()) {
  18.   }
  19.  
  20.   /**
  21.    * This function controls what to return to the contextual filter.
  22.    */
  23.   function get_argument() {
  24.   }
  25.  
  26.   /**
  27.    * Initialize this plugin with the view and the argument is is linked to.
  28.    */
  29.   function init(&$view, &$argument, $options) {
  30.     parent::init($view, $argument, $options);
  31.   }
  32. ?>

First of all, we define the option form of the handler. We want to configure which flags or vocabularys we want to use later. This makes it possible to add other flags or vocabularies very easily. Furthermore, it should be possible to add multiple terms and also to define a fallback if there is no content available. 

To achieve this we get all flags from the system and create a checkboxes form item. It is important to implement the "default value" of the options correctly. The options are available in the views object:

  1. <?php
  2. '#default_value' => $this->options['flags'],
  3. ?>

The complete function:

[gist:5264081]

As with all forms we also can add a validate and a submit function. We only add a submit function for now.

  1. <?php
  2.   /**
  3.    * Provide the default form form for submitting options.
  4.    */
  5.   function options_submit(&$form, &$form_state, &$options = array()) {
  6.     // We filter the options on only selected ones.
  7.     $options['flags'] = array_filter($options['flags']);
  8.     $options['vocabularies'] = array_filter($options['vocabularies']);
  9.   }

Now we have to override the get_argument() function. This function will return values to the filter in the view.

We compare the flagged content with the values from the options. We filter all term ids by their vocabularies using a helper function. It is important to return the value as a string just as we would call the argument by hand.

  1. <?php
  2.   /**
  3.    * This function controls what to return to the contextual filter.
  4.    */
  5.   function get_argument() {
  6.     // Get available flag types from the system.
  7.     $flags = flag_get_flags('taxonomy_term');
  8.  
  9.     // Get all User flags.
  10.     $user_flags = flag_get_user_flags('taxonomy_term');
  11.    
  12.     // This array will collect all Term IDs which will be filtered.
  13.     $tids = array();
  14.    
  15.     // Get the vocab foreach flag.
  16.     foreach ($flags as $flagname => $flag) {
  17.       // We only proceed with this flag, if it has been selected and the current
  18.       // user has flagged terms with that flag.
  19.       if (!empty($this->options['flags'][$flagname]) && !empty($user_flags[$flagname])) {
  20.         // Get all tids from the user flags.
  21.         $user_tids = array_keys($user_flags[$flagname]);
  22.         $vocabs = array();
  23.         // Check  which vocabularies are valid for this handler.
  24.         foreach ($flag->types as $vocab) {
  25.           if (!empty($this->options['vocabularies'][$vocab])) {
  26.             $vocabs[] = $vocab;
  27.           }
  28.         }
  29.         // We add the valid terms of the flag set to our default argument list.
  30.         $valid_tids = _views_flag_context_filter_tids_on_vocabularies($user_tids, $vocabs);
  31.         $tids = array_merge($tids, $valid_tids);
  32.       }
  33.     }
  34.     // If no tids are valid, we can fallback to a given value.
  35.     if (empty($tids)) {
  36.       // Fall back to the exception value (by default this is 'all')
  37.       return $this->options['fallback'];
  38.     }
  39.     // If there are term ids available we return them by concating the terms
  40.     // with the multiple operator (AND or OR).
  41.     else {
  42.       return implode($this->options['multiple_operator'], $tids);
  43.     }
  44.   }

The helper function for the .module file:

  1. <?php
  2. /**
  3.  * Helper to filter tids on given vocabularies.
  4.  *
  5.  * @param array $tids
  6.  *   array of term ids
  7.  * @param array $vocabularies
  8.  *   array of vocabulary machine names
  9.  *
  10.  * @return array
  11.  *   array of terms that live in one of the given vocabularies.
  12.  */
  13. function _views_flag_context_filter_tids_on_vocabularies($tids, $vocabularies) {
  14.   $query = db_select('taxonomy_term_data', 't')
  15.     ->fields('t', array('tid'))
  16.     ->condition('t.tid', $tids);
  17.   $query->innerJoin('taxonomy_vocabulary', 'v', 't.vid = v.vid');
  18.   $query->condition('v.machine_name', $vocabularies);
  19.   return $query->execute()->fetchCol();
  20. }

This is all for the handler.

Next, we add the handler to the contextual filter we recently created.

After applying the changes there should be the article which matches our favorite pet in the preview.

Summary

The Flag modules makes it easy to handle user preferences. Page elements can react to different flags and return relevant content to the user by using the handler we created. With a few tricks we can extend the functionality, i.e. adding automatic flagging with rules reacting on content views.

Github: https://github.com/Cyberschorsch/views_flag_context

Lucio Waßill
  • Head of Development
  • Drupal Developer
  • Scrum-Master

Lucio is originally from Leipzig and, unlike the other East Germans on the team, has not yet fled to the West. Since 2007 Lucio works with Drupal and is both an all-rounder in Drupal development as well as in project management and DevOps.

If he does not work for undpaul, he is a music junkie and runs a small online music magazine. He is also passionate about playing volleyball and looks after other volleyball players aged 18-64.

In addition, Lucio has studied Business Informatics, is Acquia Certified Developer and certified Scrum Master.