Kontextbasierter Inhalt mit Flag-Modul und eigenem Views-Default-Argument-Handler

Das Views-Modul ist von Haus aus ein sehr mächtiger Partner für das Erstellen von Drupal-Webseiten. Es ist das am häufigsten installierte Modul und wurde in Drupal 8 aufgrund seiner vielfältigen Möglichkeiten in den Core integriert.

Besonders hervorzuheben ist die Möglichkeit, die Funktionalität von Views durch das komplexe Pluginsystem zu erweitern. Mit Hilfe von Handlern, Filtern und anderen Plugins können eigene Use-Cases durch Views abgedeckt werden. 

In diesem Blogbeitrag möchte ich anhand eines ausgedachten Beispiels erläutern, wie man einen "Default Argument Handler" schreibt und ein einfaches Kontext-System mit Hilfe des Flag-Moduls erstellt.

Hinweis: Der Code des fertigen Moduls befindet sich zum Testen der Funktionalität auch auf Github.

Der Use-Case

Der Kunde produziert im Familienbetrieb biologisches Tierfutter und möchte seinen Absatz nun über den Onlinehandel erweitern. Desweiteren vertreibt die Tochter des Kunden für Katzen und Hunde jeweils ein monatlich erscheinendes Magazin mit Tipps und Tricks. Die Magazine sollen auf der Seite des Kunden mit beworben werden. Ältere Artikel aus dem Magazin werden später online zur Verfügung gestellt.

Ein Hauptziel soll es sein, dass der User nur für seine Lieblingstierart ausgewählte Produkte und Artikel angezeigt bekommen soll. Über das Flag-Modul markieren wir pro Benutzer, welche Tierarten bevorzugt werden.

Hinweis: Der Einfachheit halber werden wir die Tierarten als Benutzer manuell wählen. 

Vorbereitung

Anlegen der Flags

Zunächst legen wir ein Vokabular "Flag Context" mit beliebigen Begriffen (Terms) an, die später dann geflaggt werden.

Anschließend erstellen wir unter admin/structure/flags eine neue Flag mit Flagtype "Taxonomy Term", der als Bundle "Flag Context" auswählt.

Inhaltstyp erweitern

Damit wir später einer Flag Inhalte zuweisen können, fügen wir ein "Entity Reference"-Feld zu einem Inhaltstyp (hier "Article" aus der Standardinstallation) hinzu, damit wir Inhalte mit den Terms verlinken können. Testweise legen wir je Term dann mindestens einen Node an. Für den Use-Case würden wir dieses Field ebenso an Produkte anfügen, uns reicht nun aber erst mal das Beispiel mit dem Inhaltstypen.

Anschließend flaggen wir einen Term, damit wir später die Funktionalität testen können. Dazu gehen wir auf eine Termseite und markieren den Term.

Views erstellen

Anschließend erstellen wir zur Visualisierung einen View, um dem Benutzer später seine Präferenzen anzeigen zu können.

Der View filtert "Taxonomy Terms" auf das Vokabular "Flag Context" und hat als Relationship "Flags: Taxonomy Term flag" auf den "Flag Context" und den "Current User". Als Display nutzen wir "Block".

Zusätzlich zum Titel blenden wir einen Link ein, damit die Flag auch unflagged werden kann. 

Der Block wird testweise in die "Sidebar First" geschoben. Zum Schluss der Vorbereitung erstellen wir die Basis für den späteren Funktionstest. Dazu erstellen wir einen View mit Page Display, der Inhalte vom Typ "Article" filtert. Als "Contextual Filter" nutzen wir "Content: has taxonomy term ID". Diesen "Contextual Filter" erweitern wir später mit der gewünschten Kontextfunktion.

Programmieren des Handlers

Views-Handler müssen durch ein Modul eingebunden werden, damit es durch das Plugin-System registriert wird. Dazu ist nicht viel nötig, bietet hier aber die meisten Schwierigkeiten wenn es darum geht, herauszufinden, warum der eben erstellte Handler nicht zur Verfügung steht.

1. Die .info-Datei

Es bietet sich an, Views-Handler und -Plugins in jeweils eigene Unterverzeichnisse zu legen. Damit Drupal die Dateien findet, müssen wir in der ".info"-Datei den Ort des Handlers angeben! 

  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. Die .module-Datei

Damit sich Views für unser Modul interessiert, müssen wir zunächst hook_views_api() einbinden:

  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.  

Views sucht dann nach einer Datei, die für das Modul relevanten Code zur Verfügung stellt. Übliche Praxis ist es, das Ganze in eine modulename.views.inc Datei auszulagern.

3. Die .views.inc-Datei

In einem Array sagen wir Views, welche Art von Plugins wir benutzen möchten und wo der Code für das jeweilige Plugin zu finden ist. Dazu verwenden wir den hook_views_plugins()

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

Mit diesen 3 Schritten haben wir unseren Handler vorbereitet. Nun geht es daran, den Handler mit Funktionalität zu füllen.

4. Die views_argument_default_flag.inc-Datei

In dieser Datei definieren wir den Handler als Klasse, indem wir von views_plugin_argument_default die Methoden erben und mit unserer Logik überschreiben. 

Der Baum:

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

Zunächst definieren wir eine Form für die Einstellungen des Handlers. Wir wollen frei konfigurieren können, welche Flags und welche Vokabulare wir nutzen, um später einfacher weitere Flags und Vokabulare hinzufügen zu können. Desweiteren sollte man mehrere Terms übergeben und diese unterschiedliche kombinieren können. Außerdem sollte es eine Möglichkeit geben, einen Fallback zu definieren, falls es keinen Inhalt gibt. Dazu holen wir uns zunächst alle verfügbaren Flags aus dem System und bilden ein Checkboxes-Element. 

Wichtig ist hier, dass wir den "Default Value" korrekt implementieren. Über das Views-Objekt sind die entsprechenden Optionen verfügbar. 

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

Die fertige Funktion:

  1. <?php
  2. /**
  3.    * Define the options form to enable selection of flags.
  4.    */
  5. function options_form(&$form, &$form_state) {
  6.  
  7.     // Load all flags and types of each flag.
  8.     $flags = flag_get_flags('taxonomy_term');
  9.     // Combine all types (=vocabs) into one option array.
  10.     $flag_types = array();
  11.     $flag_options = array();
  12.     foreach ($flags as $flagname => $value) {
  13.       // $flag_types is the array storing all vocabs.
  14.       $flag_types = array_merge($flag_types, $value->types);
  15.       // $flag_options is the array storing all possible Flags.
  16.       $flag_options[$flagname] = $flagname;
  17.     }
  18.     $form['flags'] = array(
  19.       '#type' => 'checkboxes',
  20.       '#title' => t('Choose the Flags'),
  21.       '#options' => $flag_options,
  22.       '#process' => array('form_process_checkboxes'),
  23.       '#default_value' => $this->options['flags'],
  24.     );
  25.     $form['vocabularies'] = array(
  26.       '#type' => 'checkboxes',
  27.       '#title' => t('Choose the Vocabularies'),
  28.       '#options' => drupal_map_assoc($flag_types),
  29.       '#process' => array('form_process_checkboxes'),
  30.       '#default_value' => $this->options['vocabularies'],
  31.     );
  32.     $form['fallback'] = array(
  33.       '#type' => 'textfield',
  34.       '#title' => t('Fallback value'),
  35.       '#description' => t('Value to use, when no item is flagged'),
  36.       '#default_value' => $this->options['fallback'],
  37.     );
  38.     $form['multiple_operator'] = array(
  39.       '#type' => 'select',
  40.       '#options' => array(
  41.         '+' => 'OR',
  42.         ',' => 'AND',
  43.       ),
  44.       '#title' => t('Operator for multiple values'),
  45.       '#default_value' => $this->options['multiple_operator'],
  46.     );
  47.   }

  1. <?php
  2.   /**
  3.    * Return all possible options for the view and provide default values.
  4.    */
  5.   function option_definition() {
  6.     $options = parent::option_definition();
  7.     $options['flags'] = array('default' => array());
  8.     $options['vocabularies'] = array('default' => array());
  9.     $options['fallback'] = array('default' => 'all');
  10.     $options['multiple_operator'] = array('default' => '+');
  11.     return $options;
  12.   }

Wie jede Form können wir auch eine Validate- und eine Submit-Funktion definieren. Wir beschränken uns hier auf den Submit:

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

Anschließend müssen wir die Funktion get_argument() überschreiben. Diese Funktion kümmert sich später um die Rückgabe der Werte an den Filter.

In der Funktion vergleichen wir die vom Nutzer geflaggten Inhalte mit den Werten aus den Optionen. Dazu filtern wir alle Term-IDs mit den jeweiligen Vokabularen in einer Hilfsfunktion. Wichtig dabei ist, dass das Ergebnis als String zurückgegeben wird, also eben genauso, als ob man das Argument per Hand nutzt. 

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

Die Hilfsfunktion für die .module-Datei:

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

Damit wäre der Handler fertig. 

Nun fügen wir den Handler noch zum Contextual-Filter im zuletzt erstellten View hinzu.

Anschließend sollte in der Vorschau bereits der Artikel zu dem Tier sein, welches wir vorhin geflaggt haben.

Zusammenfassung

Mit Hilfe von Flags lassen sich Nutzerpräferenzen einfach verwalten. Mit den richtigen Mitteln können Seitenelemente auf diese Flags reagieren und relevanten Inhalt je Nutzer anzeigen. Mit Hilfe des hier vorgestellten Handlers können dynamische Blöcke einfach erstellt und erweitert werden. Durch ein paar zusätzliche Tricks lässt sich noch mehr aus der Funktionalität herausholen, z. B. lässt sich das Flaggen beim Betrachten von Artikeln mit Rules automatisieren.

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

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

Unser Musikjunkie Lucio hat sich im Jahr 2007 von Drupal zur Webentwicklung bekehren lassen und verstärkt das undpaul-Team seit 2012. In seiner Freizeit betreibt er mit viel Leidenschaft ein Online-Musikmagazin oder organisiert Wandertouren und Spieleabende.

Darüber hinaus hat Lucio Wirtschaftsinformatik studiert, ist Acquia Certified Developer seit 2014 und seit 2018 zertifizierter Scrum Master.