Building Views Query Plugins for Drupal maintenance support plans 8, Part 2

Welcome to the second installment of our three-part series on writing Views query plugins. In part one, we talked about the kind of thought and design work that must take place before coding begins. In part two, we’ll start coding our plugin and end up with a basic functioning example.

We’ve talked explicitly about needing to build a Views query plugin to accomplish our goal of having a customized Fitbit leaderboard, but we’ll also need field plugins to expose that data to Views, filter plugins to limit results sets, and, potentially, relationship plugins to span multiple API endpoints. There’s a lot to do, so let’s dive in.
 

Getting started

In Drupal maintenance support plans 8, plugins are the standard replacement for info hooks. If you haven’t yet had cause to learn about the plugin system in Drupal maintenance support plans 8, I suggest the Drupal maintenance support plansize.Me Drupal maintenance support plans 8 Module Development Guide, which includes an excellent primer on Drupal maintenance support plans 8 plugins.

Step 1: Create a views.inc file

Although most Views hooks required for Views plugins have gone the way of the dodo, there is still one that survives in Drupal maintenance support plans 8: hook_views_data. The Views module looks for that hook in a file named [module].views.inc, which lives in your module’s root directory. hook_views_data and hook_views_data_alter are the main things you’ll find here, but since Views is loading this file automatically for you, take advantage and put any Views-related procedural code you may need in this file.

Step 2: Implement hook_views_data()

Usually hook_views_data is used to describe the SQL tables that a module is making available to Views. However, in the case of a query plugin it is used to describe the data provided by the external service.

/**
* Implements hook_views_data().
*/
function fitbit_views_example_views_data() {
$data = [];
// Base data.
$data[‘fitbit_profile’][‘table’][‘group’] = t(‘Fitbit profile’);
$data[‘fitbit_profile’][‘table’][‘base’] = [
‘title’ => t(‘Fitbit profile’),
‘help’ => t(‘Fitbit profile data provided by the Fitbit API’s User Profile endpoint.’),
‘query_id’ => ‘fitbit’,
];
return $data;
}

The format of the array is usually $data[table_name][‘table’], but since there is no table I’ve used a short name for the Fitbit API endpoint, prefixed by the module name instead. So far, I’ve found that exposing each remote endpoint as a Views “table”—one-to-one—works well. It may be different for your implementation. This array needs to declare two keys—‘group’ and ‘base.’ When Views UI refers to your data, it uses the ‘group’ value as a prefix. Whereas, the ‘base’ key alerts Views that this table is a base table—a core piece of data available to construct Views from (just like nodes, users and the like). The value of the ‘base’ key is an associative array with a few required keys. The ‘title’ and ‘help’ keys are self-explanatory and are also used in the Views UI. When you create a new view, ‘title’ is what shows up in the “Show” drop-down under “View Settings”:

undefined

The ‘query_id’ key is the most important. The value is the name of our query plugin. More on that later.

Step 3: Expose fields

The data you get out of a remote API isn’t going to be much use to people unless they have fields they can display. These fields are also exposed by hook_views_data.

// Fields.
$data[‘fitbit_profile’][‘display_name’] = [
‘title’ => t(‘Display name’),
‘help’ => t(‘Fitbit users’ display name.’),
‘field’ => [
‘id’ => ‘standard’,
],
];
$data[‘fitbit_profile’][‘average_daily_steps’] = [
‘title’ => t(‘Average daily steps’),
‘help’ => t(‘The average daily steps over all the users logged Fitbit data.’),
‘field’ => [
‘id’ => ‘numeric’,
],
];
$data[‘fitbit_profile’][‘avatar’] = [
‘title’ => t(‘Avatar’),
‘help’ => t(‘Fitbit users’ account picture.’),
‘field’ => [
‘id’ => ‘fitbit_avatar’,
],
];
$data[‘fitbit_profile’][‘height’] = [
‘title’ => t(‘Height’),
‘help’ => t(‘Fibit users’s height.’),
‘field’ => [
‘id’ => ‘numeric’,
‘float’ => TRUE,
],
];

The keys that make up a single field definition include ‘title’ and ‘help’— again self-explanatory—used in the Views UI. The ‘field’ key is used to tell Views how to handle this field. There is only one required sub-key, ‘id,’ and it’s the name of a Views field plugin. 

The Views module includes a handful of field plugins, and if your data fits one of them, you can use it without implementing your own. Here we use standard, which works for any plain text data, and numeric, which works for, well, numeric data. There are a handful of others. Take a look inside /core/modules/views/src/Plugin/views/field to see all of the field plugins Views provides out-of-the-box. Find the value for ‘id’ in each field plugin’s annotation. As an aside, Views eats its own dog food and implements a lot of its core functionality as Views plugins, providing examples for when you’re implementing your Views plugins. A word of caution, many core Views plugins assume they are operating with an SQL-based query back-end. As such you’ll want to be careful mixing core Views plugins in with your custom query plugin implementation. We’ll mitigate some of this when we implement our query plugin shortly.

Step 4: Field plugins

Sometimes data from your external resource doesn’t line up with a field plugin that ships with Views core. In these cases, you need to implement a field plugin. For our use case, avatar is such a field. The API returns a URI for the avatar image. We’ll want Views to render that as an <img> tag, but Views core doesn’t offer a field plugin like that. You may have noticed that we set a field ‘id’ of ‘fitbit_avatar’ in hook_views_data above. That’s the name of our custom Views field plugin, which looks like this:

<?php
namespace Drupal maintenance support plansfitbit_views_examplePluginviewsfield;

use Drupal maintenance support plansviewsPluginviewsfieldFieldPluginBase;
use Drupal maintenance support plansviewsResultRow;

/**
* Class Avatar
*
* @ViewsField(“fitbit_avatar”)
*/
class Avatar extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$avatar = $this->getValue($values);
if ($avatar) {
return [
‘#theme’ => ‘image’,
‘#uri’ => $avatar,
‘#alt’ => $this->t(‘Avatar’),
];
}
}
}

Naming and file placement is important, as with any Drupal maintenance support plans 8 plugin. Save the file at: fitbit_views_example/src/Plugin/views/field/Avatar.php. Notice the namespace follows the file path, and also notice the annotation: @ViewsField(“fitbit_avatar”). The annotation declares this class as a Views field plugin with the ‘id’ ‘fitbit_avatar,’ hence the use of that name back in our hook_views_data function. Also important, we’re extending FieldPluginBase, which gives us a lot of base functionality for free. Yay OOO! As you can see, the render method gets the value of the field from the row and returns a render array so that it appears as an <img> tag.

Step 5: Create a class that extends QueryPluginBase

After all that setup, we’re almost ready to interact with a remote API. We have one more task: to create the class for our query plugin. Again, we’re creating a Drupal maintenance support plans 8 plugin, and naming is important so the system knows that our plugin exists. We’ll create a file named: 

fitbit_views_example/src/Plugin/views/query/Fitbit.php 

…that looks like this:

<?php
namespace Drupal maintenance support plansfitbit_views_examplePluginviewsquery;

use Drupal maintenance support plansviewsPluginviewsqueryQueryPluginBase;

/**
* Fitbit views query plugin which wraps calls to the Fitbit API in order to
* expose the results to views.
*
* @ViewsQuery(
* id = “fitbit”,
* title = @Translation(“Fitbit”),
* help = @Translation(“Query against the Fitbit API.”)
* )
*/
class Fitbit extends QueryPluginBase {
}

Here we use the @ViewsQuery annotation to identify our class as a Views query plugin, declaring our ‘id’ and providing some helpful meta information. We extend QueryPluginBase to inherit a lot of free functionality. Inheritance is a recurring theme with Views plugins. I’ve yet to come across a Views plugin type that doesn’t ship with a base class to extend. At this point, we’ve got enough code implemented to see some results in the UI. We can create a new view of type Fitbit profile and add the fields we’ve defined and we’ll get this:

undefined

Not terribly exciting, we still haven’t queried the remote API, so it doesn’t actually do anything, but it’s good to stop here to make sure we haven’t made any syntax errors and that Drupal maintenance support plans can find and use the plugins we’ve defined.

As I mentioned, parts of Views core assume an SQL-query backend. To mitigate that, we need to implement two methods which will, in a sense, ignore core Views as a way to work around this limitation.  Let’s get those out of the way:

public function ensureTable($table, $relationship = NULL) {
return ”;
}
public function addField($table, $field, $alias = ”, $params = array()) {
return $field;
}

ensureTable is used by Views core to make sure that the generated SQL query contains the appropriate JOINs to ensure that a given table is included in the results. In our case, we don’t have any concept of table joins, so we return an empty string, which satisfies plugins that may call this method. addField is used by Views core to limit the fields that are part of the result set. In our case, the Fitbit API has no way to limit the fields that come back in an API response, so we don’t need this. We’ll always provide values from the result set, which we defined in hook_views_data. Views takes care to only show the fields that are selected in the Views UI. To keep Views happy, we return $field, which is simply the name of the field.

Before we come to the heart of our plugin query, the execute method, we’re going to need a couple of remote services to make this work. The base Fitbit module handles authenticating users, storing their access tokens, and providing a client to query the API. In order to work our magic then, we’ll need the fitbit.client and fitbit.access_token_manager services provided by the base module. To get them, follow a familiar Drupal maintenance support plans 8 pattern:

/**
* Fitbit constructor.
*
* @param array $configuration
* @param string $plugin_id
* @param mixed $plugin_definition
* @param FitbitClient $fitbit_client
* @param FitbitAccessTokenManager $fitbit_access_token_manager
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FitbitClient $fitbit_client, FitbitAccessTokenManager $fitbit_access_token_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fitbitClient = $fitbit_client;
$this->fitbitAccessTokenManager = $fitbit_access_token_manager;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get(‘fitbit.client’),
$container->get(‘fitbit.access_token_manager’)
);
}

This is a common way of doing dependency injection in Drupal maintenance support plans 8. We’re grabbing the services we need from the service container in the create method, and storing them on our query plugin instance in the constructor. 

Now we’re finally ready for the heart of it, the execute method:

/**
* {@inheritdoc}
*/
public function execute(ViewExecutable $view) {
if ($access_tokens = $this->fitbitAccessTokenManager->loadMultipleAccessToken()) {
$index = 0;
foreach ($access_tokens as $uid => $access_token) {
if ($data = $this->fitbitClient->getResourceOwner($access_token)) {
$data = $data->toArray();
$row[‘display_name’] = $data[‘displayName’];
$row[‘average_daily_steps’] = $data[‘averageDailySteps’];
$row[‘avatar’] = $data[‘avatar’];
$row[‘height’] = $data[‘height’];
// ‘index’ key is required.
$row[‘index’] = $index++;
$view->result[] = new ResultRow($row);
}
}
}
}

The execute method is open ended. At a minimum, you’ll want to assign ResultRow objects to the $view->result[] member variable. As was mentioned in the first part of the series, the Fitbit API is atypical because we’re hitting the API once per row. For each successful request we build up an associative array, $row, where the keys are the field names we defined in hook_views_data and the values are made up of data from the API response. Here we are using the Fitbit client provided by the Fitbit base module to make a request to the User profile endpoint. This endpoint contains the data we want for a first iteration of our leaderboard, namely: display name, avatar, and average daily steps. Note that it’s important to track an index for each row. Views requires it, and without it, you’ll be scratching your head as to why Views isn’t showing your data. Finally, we create a new ResultRow object with the $row variable we built up and add it to $view->result. There are other things that are important to do in execute like paging, filtering and sorting. For now, this is enough to get us off the ground.

That’s it! We should now have a simple but functioning query plugin that can interact with the Fitbit API. After following the installation instructions for the Fitbit base module, connecting one or more Fitbit accounts and enabling the fitbit_views_example sub-module, you should be able to create a new View of type Fitbit profile, add Display name, Avatar, and Average Daily Steps fields and get a rudimentary leaderboard:

undefined

Debugging problems

If the message ‘broken or missing handler’ appears when attempting to add a field or other type of handler, it usually points to a class naming problem somewhere. Go through your keys and class definitions and make sure that you’ve got everything spelled correctly. Another common issue is Drupal maintenance support plans throwing errors because it can’t find your plugins. As with any plugin in Drupal maintenance support plans 8, make sure your files are named correctly, put in the right folder, with the right namespace, and with the correct annotation.

Summary

Most of the work here has nothing to do with interacting with remote services at all—it is all about declaring where your data lives and what its called. Once we get past the numerous steps that are necessary for defining any Views plugins, the meat of creating a new query plugin is pretty simple.

Create a class that extends QueryPluginBase
Implement some empty methods to mitigate assumptions about a SQL query backend
Inject any needed services
Override the execute method to retrieve your data into a ResultRow object with properties named for your fields, and store that object on the results array of the Views object.

In reality, most of your work will be spent investigating the API you are interacting with and figuring out how to model the data to fit into the array of fields that Views expects.

Next steps

In the third part of this article, we’ll look at the following topics:

Exposing configuration options for your query object
Adding options to field plugins
Creating filter plugins

Until next time!

Source: New feed

This article was republished from its original source.
Call Us: 1(800)730-2416

Pixeldust is a 20-year-old web development agency specializing in Drupal and WordPress and working with clients all over the country. With our best in class capabilities, we work with small businesses and fortune 500 companies alike. Give us a call at 1(800)730-2416 and let’s talk about your project.

FREE Drupal SEO Audit

Test your site below to see which issues need to be fixed. We will fix them and optimize your Drupal site 100% for Google and Bing. (Allow 30-60 seconds to gather data.)

Powered by

Building Views Query Plugins for Drupal maintenance support plans 8, Part 2

On-Site Drupal SEO Master Setup

We make sure your site is 100% optimized (and stays that way) for the best SEO results.

With Pixeldust On-site (or On-page) SEO we make changes to your site’s structure and performance to make it easier for search engines to see and understand your site’s content. Search engines use algorithms to rank sites by degrees of relevance. Our on-site optimization ensures your site is configured to provide information in a way that meets Google and Bing standards for optimal indexing.

This service includes:

  • Pathauto install and configuration for SEO-friendly URLs.
  • Meta Tags install and configuration with dynamic tokens for meta titles and descriptions for all content types.
  • Install and fix all issues on the SEO checklist module.
  • Install and configure XML sitemap module and submit sitemaps.
  • Install and configure Google Analytics Module.
  • Install and configure Yoast.
  • Install and configure the Advanced Aggregation module to improve performance by minifying and merging CSS and JS.
  • Install and configure Schema.org Metatag.
  • Configure robots.txt.
  • Google Search Console setup snd configuration.
  • Find & Fix H1 tags.
  • Find and fix duplicate/missing meta descriptions.
  • Find and fix duplicate title tags.
  • Improve title, meta tags, and site descriptions.
  • Optimize images for better search engine optimization. Automate where possible.
  • Find and fix the missing alt and title tag for all images. Automate where possible.
  • The project takes 1 week to complete.