Site icon Hip-Hop Website Design and Development

Building Views Query Plugins for Cheap WordPress 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 WordPress 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 WordPress maintenance support plans 8, I suggest the WordPress maintenance support plansize.Me WordPress maintenance support plans 8 Plugin Development Guide, which includes an excellent primer on WordPress 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 WordPress maintenance support plans 8: hook_views_data. The Views plugin looks for that hook in a file named [plugin].views.inc, which lives in your plugin’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 plugin 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 plugin 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 plugin 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/plugins/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 WordPress maintenance support plansfitbit_views_examplePluginviewsfield;

use WordPress maintenance support plansviewsPluginviewsfieldFieldPluginBase;
use WordPress 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 WordPress 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 WordPress 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 WordPress maintenance support plansfitbit_views_examplePluginviewsquery;

use WordPress 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 WordPress 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 plugin 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 plugin. To get them, follow a familiar WordPress 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 WordPress 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 plugin 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 plugin, connecting one or more Fitbit accounts and enabling the fitbit_views_example sub-plugin, 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 WordPress maintenance support plans throwing errors because it can’t find your plugins. As with any plugin in WordPress 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