Custom Views field

Now that we have seen how data is exposed to Views, we can start understanding the NodeViewsData handler I mentioned earlier (even if not quite everything) a bit better. But this also provides a good segue back to our Product entity type's views_data handler, where we can now see what the responsibility of getViewsData() is. It needs to return the definition for all of the tables and fields, as well as what they can do. Luckily for us, the base class already provides everything we need to turn our product data into Views fields, filters, sorts, arguments, and potentially relationships, all out of the box.

But let's say we want to add some more Views fields that make sense to us in the context of our product-related functionality. For example, each product has a source field that is populated by the Importer entity from its own source field. This is just to keep track of where they come from. So we may want to create a Views field that simply renders the name of the Importer that has imported the product.

You'll be quick to ask: But hey, that is not a column on the products table! What gives? As we will see, we can define Views fields that render whatever data we want (that can relate to the record or not). Of course, this also means that the resulting data cannot be used inside a sort or filter because MySQL doesn't have access to it when building the query. So we are a bit less flexible there, but it makes sense.

In this section, you will learn two things. First, we'll see how to create our own views_data handler for our Product entity type. By now, you should be quite familiar with this process. More importantly though, we'll use this handler to create a new Views field for our products that renders something no existing ViewsField plugin can offer: the name of the related Importer entity. That means our own custom plugin. How exciting, so let's get going!

There are two quick steps to create our own views_data handler. First, we need the class:

namespace Drupal\products\Entity; 
 
use Drupal\views\EntityViewsData; 
 
/** 
 * Provides Views data for Product entities. 
 */ 
class ProductViewsData extends EntityViewsData { 
 
  /** 
   * {@inheritdoc} 
   */ 
  public function getViewsData() { 
    $data = parent::getViewsData(); 
    // Add stuff. 
    return $data; 
  } 
}  

As you can see, we are extending the base EntityViewsData class we had been referencing in the Product entity type annotation before. Inside, we are overriding the getViewsData() method to add our own definitions (which will go where you can see the comment).

Second, we need to change the handler reference to this new class in the entity type annotation:

"views_data" = "Drupal\products\Entity\ProductViewsData", 

That's it. We can now define our own custom fields and we can start with the views data definition:

$data['product']['importer'] = [ 
  'title' => t('Importer'), 
  'help' => t('Information about the Product importer.'), 
  'field' => array( 
    'id' => 'product_importer', 
  ), 
]; 

Simple stuff, like we did with the players. Except in this case, we are adding it to the product table and we are using a ViewsField plugin that doesn't exist. Yet. So, let's create it.

As you may have noticed if you checked some of the existing ones, Views plugins go in the Plugin\views\[plugin_type] namespace of the modules, where [plugin_type] in this case is field, as we are creating a ViewsField plugin. So, we can start with the plugin class scaffolding:

namespace Drupal\products\Plugin\views\field; 
 
use Drupal\views\Plugin\views\field\FieldPluginBase; 
use Drupal\views\ResultRow; 
 
/** 
 * Field plugin that renders data about the Importer that imported the Product. 
 * 
 * @ViewsField("product_importer") 
 */ 
class ProductImporter extends FieldPluginBase { 
 
  /** 
   * {@inheritdoc} 
   */ 
  public function render(ResultRow $values) { 
    // Render something more meaningful. 
    return ''; 
  } 
}  

Just like any other field plugin, we are extending the FieldPluginBase class which provides all the common defaults and base functionalities the fields need. Of course, you notice the admittedly small annotation, which simply contains the plugin ID. Our main job is to work in the render() method and output something, preferably using the $values object that contains all the data in the respective row.

Inside the ResultRow object, we can find the values from the Views row which can contain multiple fields. In case it's a View that lists entities, we also have an _entity key that references the entity object itself.

Clearing the cache, we will now be able to add the new Product Importer field to a View for products. But if we do, we will notice an error. Views is trying to add to the query the product_importer field we defined but which doesn't actually exist on the table. That isn't right! This happens because, even though Views can be made to work with any data source, it still has a preference for the SQL database, so we can encounter these issues every once in a while. Not to worry though, as we can simply tell our plugin not to include the field in any query—it will show totally custom data. We do so by overriding the query() method:

/** 
 * {@inheritdoc} 
 */ 
public function query() { 
  // Leave empty to avoid a query on this field. 
}  

That's it. Now, our field is going to render an empty string:''. Let's change it to look for the related Importer entity and show its label. But in order to do that, we'll need the EntityTypeManager service to use for querying. Let's inject it:

/** 
 * @var \Drupal\Core\Entity\EntityTypeManager 
 */ 
protected $entityTypeManager; 
 
/** 
 * Constructs a ProductImporter object. 
 * 
 * @param array $configuration 
 *   A configuration array containing information about the plugin instance. 
 * @param string $plugin_id 
 *   The plugin_id for the plugin instance. 
 * @param mixed $plugin_definition 
 *   The plugin implementation definition. 
 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager 
 */ 
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entityTypeManager) { 
  parent::__construct($configuration, $plugin_id, $plugin_definition); 
  $this->entityTypeManager = $entityTypeManager; 
} 
 
/** 
 * {@inheritdoc} 
 */ 
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 
  return new static( 
    $configuration, 
    $plugin_id, 
    $plugin_definition, 
    $container->get('entity_type.manager') 
  ); 
}  

Since we are operating inside a plugin, we need to make sure we are implementing the ContainerFactoryPluginInterface in order to make use of the create() method. But luckily, a parent class does so already, namely Drupal\views\Plugin\views\PluginBase, so we're good.

We do, however, have to also add the new use statements at the top:

use Drupal\Core\Entity\EntityTypeManagerInterface; 
use Symfony\Component\DependencyInjection\ContainerInterface; 

We can now proceed with the render() method:

public function render(ResultRow $values) { 
  /** @var \Drupal\products\Entity\ProductInterface $product */ 
  $product = $values->_entity; 
  $source = $product->getSource(); 
  $importers = $this->entityTypeManager->getStorage('importer')->loadByProperties(['source' => $source]); 
  if (!$importers) { 
    return NULL; 
  } 
 
  // We'll assume one importer per source. 
  /** @var \Drupal\products\Entity\ImporterInterface $importer */ 
  $importer = reset($importers); 
  return $this->sanitizeValue($importer->label()); 
}  

We simply get the Product entity of the current row and then query for the Importer configuration entities that have the source referenced on the product. We assume there is only one (even if we did not do a proper job ensuring this is the case to save some space) and simply return its label. We also pass it through the helper sanitizeValue() method which takes care of ensuring that the output is safe against XSS attacks and such. So now our products View can show, for each product, the name of the Importer that brought them into application.

If we take a step back and try to understand what is going on, a word of caution becomes evident. Views performs one big query that returns a list of product entities and some data. But then, when that data is output, we perform a query for the Importer entity corresponding to each product in the result set (and we load those entities). So if we have 100 products returned, that means 100 more queries. Try to keep this in mind when creating custom fields to ensure you are not getting a huge performance hit, which might often not even be worth it.