Altering views' results

Submitted by on

The Views module provides a flexible method for Drupal site builders to present data. On a recent project we needed to filter a view's result set in a way we could not achieve by means of the module's UI. How do you programmatically alter a view's result set before rendering? Let's see how to do it using the hooks provided by the module.

The need surfaced while working on the web site for MIT's Global Studies and Languages department, which uses Views to pull in data from a remote service and display it. The Views module provides a flexible method for Drupal site builders to present data. Most of the time you can configure your presentation needs through the UI using Views and Views-related contributed modules. Notwithstanding, sometimes you need to implement a specific requirement which is not available out of the box. Luckily, Views provides hooks to alter its behavior and results. Let’s see how to filter Views results before they are rendered.

Assume we have a website which aggregates book information from different sources. We store the book name, author, year of publication, and ISBN (International Standard Book Number). ISBNs are unique numerical book identifiers which can be 10 or 13 characters long. The last digit in either version is a verification number and the 13 character version has a 3-character prefix. The other numbers are the same. A book can have both versions. For example:

ISBN10:    1849511160
ISBN13: 9781849511162

In our example website, we only use one ISBN. If both versions are available, the 10-character version is discarded. We do this to prevent duplicate book entries which differ only in ISBN as shown in the following picture:

To remove the duplicate entries, follow this simple two step process:

  1. Find the correct Views hook to implement.
  2. Add the logic to remove unwanted results.

After reviewing the list of Views hooks, hook_views_pre_render is the one we are going to use to filter results before they are rendered. Now, let’s create a custom module to add the required logic. I have named my module views_alter_results so the hook implementation would look like this:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  // Custom code.
}

The ampersand in the function parameter indicates that the View object is passed by reference. Any change we make to the object will be kept. The View object has a results property. Using the devel module, we can use dsm($view->results) to have a quick look at the results.

Each element in the array is a node that will be displayed in the final output. If we expand one of them, we can see more information about the node. Let’s drill down into one of the results until we get to the ISBN.

The output will vary depending on your configuration. In this example, we have created a Book content type and added an ISBN field. Before adding the logic to filter the unwanted results, we need to make sure that this logic will only be applied for the specific view and display we are targeting. By default, hook_views_pre_render will be executed for every view and display unless otherwise instructed. We can apply this restriction as follows:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    // Custom code.
  }
}

Next, the logic to filter results.

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    $isbn10_books = array();
    $isbn13_books = array();
    $remove_books = array();

    foreach ($view->result as $index => $value) {
      $isbn = $value->field_field_isbn[0]['raw']['value'];
      if (strlen($isbn) === 10) {
        // [184951116]0.
        $isbn10_books[$index] = substr($isbn, 0, 9);
      }
      elseif (strlen($isbn) === 13) {
        // 978[184951116]2.
        $isbn13_books[$index] = substr($isbn, 3, 9);
      }
    }

    // Find books that have both ISBN10 and ISBN13 entries.
    $remove_books = array_intersect($isbn10_books, $isbn13_books);

    // Remove repeated books.
    foreach ($remove_books as $index => $value) {
      unset($view->result[$index]);
    }
  }
}

To filter the results we use unset on $view->result. After this process, the result property of the view object will look like this:

And our view will display without duplicates book entries as seen here:

Before wrapping up, I’d like to share two modules that might help you achieve similar results: Views Merge Rows and Views Distinct. Every use case is different, if neither of these modules gets you where you want to be, you can leverage hook_views_pre_render to implement your custom requirements.

Update #1 Tue, 06/02/2015

As indicated by Leon and efpapado this approach only works for views that present all results in a single page. That was the original use case. The altering presented here only affects the current page and the pager will not work as expected.

Comments

Submitted by Leon on Tue, 06/02/2015 - 10:25

Note that this will break the pager.
You want to be ideally editing the query, rather than the render results.
You could use hook_views_query_alter(), but even better if you can create Views filter plugins that achieve the same result.

Submitted by merlinofchaos on Tue, 06/02/2015 - 11:12

I think this shines a light on a flawed data model. Since the ISBN13 and ISBN10 are actually the same number (just with 978 added to the beginning) on the very same work they should be separate fields, i.e, field_isbn10 and field_isbn13, and then there's one entry. In your model, you actually have two entries for the same work.

You could then use a computed field to compute field_isbn using what is effectively a coalesce on those two fields. Why a computed field? It's easier to index a single field than it is a pair of fields, and while Views can fake a coalesce visually, you'd have to write custom filter and sort handlers to filter/sort on a COALESCE'd field, and that will also produce less efficient queries in a place that probably matters to a site's performance.

Submitted by efpapado on Tue, 06/02/2015 - 11:21

Very nice post, but I believe there is a problem with your concept in some cases.

This solution can be implemented only with views without pagination - you have to declare all results to appear in a single page.

This is because, when asking for pagination, the number of results per page is implemented in the early stages of the view building, into the SQL statement (LIMIT and OFFSET clauses).
The $view->results array contains objects of the specific page of the view, limited by pagination.

So, if you ask your view to show 10 books per page and apply this logic, if in the 1st page there is a book with both 10- and 13-characters ISBN, it will be excluded, so in the first page you will have only 9 results.
Moreover, if all books of the first page (10 books) have both 10- and 13-characters ISBN, the half of them will be excluded, so only 5 books will be shown.

Submitted by R0ber on Sun, 07/26/2015 - 04:42

Hi, I have the same problem, I can delete rows I want, but i can not change pagination.

Add new comment