1

Hi I have a requirement to display the data from external api but should not be imported into any entities. What I did is I have written a custom view query plugin which displays the data from external api. What I did is I have used the GuzzleHttp Client to get the data. Here is how my service looks like


namespace Drupal\***_services;

use Drupal\Component\Serialization\Json;

class NewsBlogsClient {

  /**
   * @var \GuzzleHttp\Client
   */
  protected $client;

  /**
   * NewsBlogsClient constructor.
   *
   * @param $http_client_factory \Drupal\Core\Http\ClientFactory
   */
  public function __construct($http_client_factory) {
    $this->client = $http_client_factory->fromOptions([
      'base_uri' => 'https://****************/',
    ]);
  }

  /**
   * Get all the news.
   *
   * @return array
   */
  public function getNews() {
    $response = $this->client->get('****');

    $data = Json::decode($response->getBody());

    return $data;
  }

  /**
   * Get all the blogs.
   *
   * @return array
   */
  public function getBlogs() {
    $response = $this->client->get('****');

    $data = Json::decode($response->getBody());

    return $data;
  }

}

I called the service into my plugin query and mapped it to the view fields that I have created using hook_views_data(). Here is how my plugin query looks like.

namespace Drupal\****\Plugin\views\query;

use Drupal\views\ViewExecutable;
use Drupal\views\ResultRow;
use Drupal\views\Annotation\ViewsQuery;
use Drupal\Core\Annotation\Translation;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Datetime\DrupalDateTime;

/**
 * Views query plugin which wraps calls to the *** API in order to
 * expose the results to views.
 *
 * @ViewsQuery(
 *   id = "news",
 *   title = @Translation("News"),
 *   help = @Translation("Query against the API.")
 * )
 */
class News extends QueryPluginBase implements ContainerFactoryPluginInterface {

  /**
   * @var \Drupal\*****\NewsBlogsClient
   */
  protected $newsBlogsClient;

  /**
   * FeaturedNews constructor.
   *
   * @param array $configuration
   * @param $plugin_id
   * @param $plugin_definition
   * @param $newsBlogsClient \Drupal\****\NewsBlogsClient
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, $news_blogs_client) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->newsBlogsClient = $news_blogs_client;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('news_blogs_client')
    );
  }

  /**
   *
   * {@inheritdoc}
   */

  public function execute(ViewExecutable $view) {
    $index = 0;
    if ($news_items = $this->newsBlogsClient->getNews()) {
      foreach ($news_items as $news_item) {
        $row['title'] = $news_item['title'];
        $row['link'] = $news_item['view_node'];
        $date = $news_item['field_published_date'];
        $dateTime = new DrupalDateTime($date);
        $timestamp = $dateTime->format('U');
        $row['pubdate'] = $timestamp;
        $row['summary'] = $news_item['field_short_description'];
        $row['thumbnail'] = $news_item['uri'];
        // 'index' key is required.
        $row['index'] = $index++;
        $view->result[] = new ResultRow($row);
      }
    }
  }

  /**
   * Ensures a table exists in the query.
   *
   * This replicates the interface of Views' default SQL backend to simplify
   * the Views integration of the API. Since the API has no
   * concept of "tables", this method implementation does nothing. If you are
   * writing API-specific Views code, there is therefore no reason at all
   * to call this method.
   * See https://www.drupal.org/node/2484565 for more information.
   *
   * @return string
   *   An empty string.
   */
  public function ensureTable($table, $relationship = NULL) {
    return '';
  }

  /**
   * Adds a field to the table. In our case, the API has no
   * notion of limiting the fields that come back, so tracking a list
   * of fields to fetch is irrelevant for us. Hence this function body is more
   * or less empty and it serves only to satisfy handlers that may assume an
   * addField method is present b/c they were written against Views' default SQL
   * backend.
   *
   * This replicates the interface of Views' default SQL backend to simplify
   * the Views integration of the API.
   *
   * @param string $table
   *   NULL in most cases, we could probably remove this altogether.
   * @param string $field
   *   The name of the metric/dimension/field to add.
   * @param string $alias
   *   Probably could get rid of this too.
   * @param array $params
   *   Probably could get rid of this too.
   *
   * @return string
   *   The name that this field can be referred to as.
   *
   * @see \Drupal\views\Plugin\views\query\Sql::addField()
   */
  public function addField($table, $field, $alias = '', $params = array()) {
    return $field;
  }
}

Everything is working great. I can display and control the data through views. The only missing thing is paging. The API is also from another Drupal application which supports pagination. I also need to setup a pager for this query. Tried lot of things but didn't work. I don't know how actually a view pager works. Can anyone explain and provide me any example snippet or code to work with.

1

You're missing some pager handling that's required. There isn't a great example of this, except to try to follow along with what \Drupal\views\Plugin\views\query\Sql does, translating the things that are SQL specific into things that are relevant for your API.

In your implementation of \Drupal\views\Plugin\views\query\QueryPluginBase::execute you need to first trigger the preExecute phase of the pager setup. Looking at the \Drupal\views\Plugin\views\query\Sql implementation, here's what that class does:

// Let the pager modify the query to add limits.
$view->pager->preExecute($query);

if (!empty($this->limit) || !empty($this->offset)) {
  // We can't have an offset without a limit, so provide a very large limit instead.
  $limit = intval(!empty($this->limit) ? $this->limit : 999999);
  $offset = intval(!empty($this->offset) ? $this->offset : 0);
  $query->range($offset, $limit);
}

Obviously the line $query->range($offset, $limit); isn't applicable to your use case, b/c here \Drupal\views\Plugin\views\query\Sql is concerned with building up a SQL query. The rest is applicable almost verbatium. With the above code you have a $limit and $offset per the configuration (and/or active query string / exposed pager) of your view that you can then apply to your API call to get back a paged result. That's the key, the views module won't page the $view->result array for you. It's up to you to use the $offset and $limit to apply paging yourself. Ideally your API supports paging and you can translate $offset and $limit to match what the API wants, or if your lucky just use them directly. Alternatively, you would need to array_slice your full array of results back from the API.

Hope that helps.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.