Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

Exposing External and Custom Data to Views with hook_views_data()

By admin, 8 November, 2024
A flowchart diagram showing how external or custom data is exposed to Drupal Views using hook_views_data(). The process begins with hook_views_data(), then moves to defining a virtual table, registering fields, passing data into the Views UI, adding fields, and finally enabling display, filtering, and sorting of the data.

When we want to make data available to Views, we need to define it in a way that Views can interpret. For content entities, the EntityViewsData::getViewsData() method accomplishes this, making data accessible for Views. However, when working with custom data, we use hook_views_data() to define how Views should handle it.

In the context of Views, a “field” isn’t limited to actual entity fields or database fields. It can represent any individual piece of data, whether it’s a column in a table, a property from an API, or even other sources. Similarly, the concept of a “table” in Views doesn’t have to be a traditional database table—it could refer to an external resource or a custom data structure.

If we’re working with a database table, Views knows how to query it directly, simplifying things. But if our data source is external, we’d need to implement the logic for querying it ourselves, typically by creating a custom ViewsQuery plugin to handle the data retrieval.

Here’s an example of creating a custom query with ViewsQuery to retrieve data from an external source.

Step 1: Create a Custom ViewsQuery Plugin

To start, we’ll create a plugin that extends ViewsQuery and defines the logic for interacting with an external API. Let’s assume we have an API that returns data in JSON format.

First, we define the file src/Plugin/views/query/ExternalApiQuery.php in our module:

namespace Drupal\my_module\Plugin\views\query;
use Drupal\views\Plugin\views\query\ViewsQueryBase;
use Drupal\views\ViewExecutable;
use Drupal\views\ResultRow;
/
* Defines a custom Views query plugin for external API.
*
* @ViewsQuery(
*   id = "external_api_query",
*   title = @Translation("External API Query"),
*   help = @Translation("Query data from an external API.")
* )
*/
class ExternalApiQuery extends ViewsQueryBase {
/
  * {@inheritdoc}
  */
 public function execute(ViewExecutable $view) {
   // Perform the request to the external API.
   $response = \Drupal::httpClient()->get('https://api.example.com/data');
   $data = json_decode($response->getBody(), TRUE);
  // Prepare results for Views.
   foreach ($data as $item) {
     $row = new ResultRow();
    // Bind data to fields defined in hook_views_data().
     $row->field_name = $item['field_name'];
     $row->another_field = $item['another_field'];
    $view->result[] = $row;
   }
 }
}

Step 2: Implement hook_views_data

Next, we need to register the data for Views so it recognizes them as available fields. This is done with hook_views_data():

/
* Implements hook_views_data().
*/
function my_module_views_data() {
 $data = [];
// Define a virtual table 'external_data' that doesn’t exist in the database.
 $data['external_data']['table'] = [
   'group' => t('External Data'),
   'provider' => 'my_module',
   'base' => [
     'field' => 'field_name',
     'title' => t('External Data'),
     'help' => t('Data from an external API.'),
   ],
 ];
// Define fields for this table.
 $data['external_data']['field_name'] = [
   'title' => t('Field Name'),
   'help' => t('A field from the external API.'),
   'field' => [
     'id' => 'standard',
   ],
 ];
$data['external_data']['another_field'] = [
   'title' => t('Another Field'),
   'help' => t('Another field from the external API.'),
   'field' => [
     'id' => 'standard',
   ],
 ];
return $data;
}

Step 3: Use in Views

After enabling the my_module, you can create a new view in the Views section and select the data source from the external API. In the Views interface, choose External API Query** as the query method, and add the Field Name and Another Field as fields.

This approach allows you to customize API queries, process the retrieved data, and provide it to Views as if it were standard Drupal fields, making them available for sorting, filtering, and display.

Module
external_data_1.zip (3.71 KB)

Tags

  • #Drupal Planet
  • Add new comment

Comments4

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Anonymous (not verified)

1 year 5 months ago

It's worth noting that there…

It's worth noting that there are contributed modules that may be the best place to start: views_database_connector, views_json_source, and views_csv_source

  • Reply

admin

7 months 4 weeks ago

In reply to It's worth noting that there… by Anonymous (not verified)

You’re absolutely right 👍 —…

You’re absolutely right 👍 — before rolling your own @ViewsQuery plugin, it’s worth checking whether an existing contributed module already solves most of the problem.

  • views_database_connector
    Lets you connect Views directly to an external database (MySQL, PostgreSQL, etc.). If your data lives in another DB, this is often the simplest solution.
  • views_json_source
    Lets you pull data from a JSON feed or REST API into Views. For your example with https://jsonplaceholder.typicode.com/comments?postId=1, this module could expose that endpoint to Views without writing custom query plugins.
  • views_csv_source
    Similar idea, but for CSV files. Useful if your external system exports CSV instead of offering an API.

✅ These modules give you a starting point with less custom code.
⚡ But if you need fine-grained control, custom hook_views_data() + @ViewsQuery is still the way to go.

  • Reply

MJ (not verified)

1 year 5 months ago

Error: the external table is not found

After implementing your solution, I get the following error just above the Views' preview window:

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'testable.external_data' doesn't exist: SELECT DISTINCT "external_data"."id" AS "external_data_id", "external_data"."field_name" AS "field_name" FROM "external_data" "external_data" LIMIT 5 OFFSET 0; Array ( )

You mentioned "In the Views interface, choose External API Query**", are you referring to the query settings in the Advanced panel? If yes, I do not have any such selection appearing for me there.

Note: I also noticed that fields defined in the views_data() hook cannot be added as filter fields.

  • Reply

admin

7 months 4 weeks ago

In reply to Error: the external table is not found by MJ (not verified)

This is a common pitfall…

This is a common pitfall when first experimenting with custom data sources in Views. Let me break down what’s happening:


Why you’re seeing

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'testable.external_data' doesn't exist
  • Your ExternalDataQuery plugin extends Sql, which means Views assumes there’s a real SQL table called external_data.
  • That’s why Views tries to generate a SELECT … FROM external_data query.
  • Since that table doesn’t exist in your DB, you get the error.

Two important clarifications

  1. You cannot use Sql if you don’t have a database table.
    Instead, you need to extend ViewsQueryBase (non-SQL query) and implement your own logic in execute().
  2. Custom query plugins don’t automatically show up in the UI.
    They must be connected via hook_views_data() where you define a base table with query_id = "external_data_query".
    Without this, Views doesn’t know that your plugin should handle the “table”.

Minimal working pattern

1. Define query plugin

<?php 

namespace Drupal\external_data\Plugin\views\query; 

use Drupal\views\Plugin\views\query\Sql; 
use Drupal\views\ViewExecutable; 
use GuzzleHttp\Exception\RequestException; 

/ 
 * @ViewsQuery( 
 *   id = "external_data_query", 
 *   title = @Translation("External Data Query") 
 * ) 
 */ 
class ExternalDataQuery extends Sql { 

  / 
   * {@inheritdoc} 
   */ 
  public function execute(ViewExecutable $view) { 
    $client = \Drupal::httpClient(); 
    $rows = []; 

    try { 
      // Perform request to external API. 
      $response = $client->get('https://jsonplaceholder.typicode.com/comments', [ 
        'query' => ['postId' => 1], 
      ]); 

      if ($response->getStatusCode() === 200) { 
        $data = json_decode($response->getBody()->getContents()); 

        // Convert JSON response into a result array for Views. 
        foreach ($data as $item) { 
          $rows[] = (object) [ 
            'id' => $item->id, 
            'name' => $item->name, 
            'email' => $item->email, 
            'body' => $item->body, 
          ]; 
        } 
      } 
    } 
    catch (RequestException $e) { 
      // Log an error if API request fails. 
      \Drupal::logger('external_data')->error('API request failed: @msg', ['@msg' => $e->getMessage()]); 
    } 

    // Pass results into the View. 
    foreach ($rows as $row) { 
      $view->result[] = $row; 
    } 
  } 
} 

👉 Notice: extends ViewsQueryBase (not Sql).


2. Define hook_views_data()

<?php 

use Drupal\views\ResultRow; 
use Drupal\views\ViewExecutable; 

/ 
 * Implements hook_views_data(). 
 */ 
function external_data_views_data() { 
  $data['external_data']['table'] = [ 
    'group' => t('External Data'), 
    'base' => [ 
      'field' => 'id', 
      'title' => t('External Data'), 
      'help' => t('Data from external source.'), 
      'query_id' => 'external_data_query', // 👈 custom query instead of a real DB table 
    ], 
  ]; 

  $data['external_data_table']['id'] = [ 
    'title' => t('ID'), 
    'help' => t('External ID'), 
    'field' => [ 
      'id' => 'numeric', 
    ], 
  ]; 
  $data['external_data_table']['name'] = [ 
    'title' => t('Name'), 
    'help' => t('External Name'), 
    'field' => [ 
      'id' => 'string', 
    ], 
  ]; 

  // Field: id 
  $data['external_data']['id'] = [ 
    'title' => t('External ID'), 
    'help' => t('ID from external source'), 
    'field' => [ 
      'id' => 'standard', 
    ], 
  ]; 

  // Field: name 
  $data['external_data']['name'] = [ 
    'title' => t('External name'), 
    'help' => t('Name from external source'), 
    'field' => [ 
      'id' => 'standard', 
    ], 
  ]; 

  return $data; 
} 

/ 
 * Implements hook_views_data_alter(). 
 */ 
function external_data_views_data_alter(array &$data) { 
  // Attach a row plugin for the external data table. 
  $data['external_table']['table']['base']['row_plugin'] = 'views_plugin_row'; 
} 

/** 
 * Implements hook_views_pre_render(). 
 * 
 * This runs before the View is rendered. 
 */ 
function external_data_views_pre_render(ViewExecutable $view) { 
  // Iterate over all result rows. 
  foreach ($view->result as $i => $row) { 

    // Skip if the row is already a ResultRow object. 
    if ($row instanceof ResultRow) { 
      continue; 
    } 

    $values = []; 

    // Map View fields to the row data. 
    foreach ($view->field as $alias => $field) { 
      // Use the alias if present on the source object. 
      if (isset($row->{$alias})) { 
        $values[$alias] = $row->{$alias}; 
      } 
      // Otherwise try the handler field name. 
      elseif (!empty($field->field) && isset($row->{$field->field})) { 
        $values[$alias] = $row->{$field->field}; 
      } 
      else { 
        $values[$alias] = NULL; 
      } 
    } 

    // Replace the raw object with a ResultRow. 
    $view->result[$i] = new ResultRow($values); 
  } 

  // Debug log (can be removed in production). 
  \Drupal::logger('external_data')->notice('<pre>@result</pre>', [ 
    '@result' => print_r($view->result, TRUE), 
  ]); 
} 

👉 Critical difference: query_id binds your fake “table” to the ExternalDataQuery plugin.


3. Add it in Views

  • In the “Show” dropdown (Advanced → Query settings), your External Data Query should now appear as a selectable data source.
  • You’ll be able to add fields like id and name.

About “fields not available as filters”

That’s expected unless you define filter handlers in hook_views_data():

$data['external_data']['id']['filter'] = [
  'id' => 'numeric',
];
$data['external_data']['name']['filter'] = [
  'id' => 'string',
];

Without those, Views only knows how to display the fields, not how to filter/sort them.


✅ So the fix:

  • Use ViewsQueryBase instead of Sql.
  • Add query_id to your base table definition.
  • Explicitly define filter/sort handlers in hook_views_data().
  • Reply
Powered by Drupal