Developing Custom Drupal Views
Views is the most powerful tool in Drupal for displaying content lists. Filters, sorts, related entities, different output formats, ajax-pagination, exposed filters — everything is configured without code. Custom code is needed where UI isn't enough: custom filters, custom field formatters, programmatic query manipulation.
Creating View via UI
Path: /admin/structure/views/add. Main parameters:
- View name — machine name, used in code
- Show — entity type (Content, Users, Taxonomy terms...)
- Tagged with — tag filter when creating
- Display — Page (URL), Block, REST Export, Feed, Attachment
View Configuration in YAML
After UI setup export:
drush config:export
# creates views.view.{machine_name}.yml
Fragment of complex View configuration:
# views.view.news_list.yml
id: news_list
label: 'News List'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_options:
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
link_to_entity: true
label: ''
element_label_colon: false
field_image:
id: field_image
table: node__field_image
field: field_image
image_style: news_teaser
image_link: content
created:
id: created
date_format: custom
custom_date_format: 'd.m.Y'
filters:
status:
value: '1'
table: node_field_data
field: status
type:
value: { news: news }
table: node_field_data
field: type
field_category_target_id:
id: field_category_target_id
exposed: true
expose:
label: 'Category'
identifier: category
sorts:
created:
order: DESC
pager:
type: full
options:
items_per_page: 12
style:
type: html_list
row:
type: 'fields'
use_ajax: true
page_1:
display_plugin: page
path: /news
display_options:
menu:
type: normal
title: News
Exposed Filters (filters for user)
Exposed filters render as form — user can filter list without reload (with AJAX) or with reload. Configured in UI by checking "Expose this filter to visitors" for each filter.
Custom exposed filter handler via hook_views_query_alter:
// my_module.module
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\query\QueryPluginBase;
/**
* Implements hook_views_query_alter().
*/
function my_module_views_query_alter(ViewExecutable $view, QueryPluginBase $query): void {
if ($view->id() !== 'news_list') return;
// Add additional condition based on GET parameter
$price_from = \Drupal::request()->query->get('price_from');
if ($price_from && is_numeric($price_from)) {
$query->addWhereExpression(
0,
"node__field_price.field_price_value >= :price_from",
[':price_from' => (float) $price_from]
);
}
}
Custom Row Style Plugin
Standard styles: Fields, Entity, HTML list, Table. For custom render — plugin:
// src/Plugin/views/row/ArticleCardRow.php
namespace Drupal\my_module\Plugin\views\row;
use Drupal\views\Plugin\views\row\RowPluginBase;
use Drupal\views\ResultRow;
/**
* @ViewsRow(
* id = "article_card",
* title = @Translation("Article card"),
* help = @Translation("Renders articles as cards"),
* display_types = {"normal"},
* )
*/
class ArticleCardRow extends RowPluginBase {
public function render(ResultRow $row): array {
$node = $row->_entity;
$view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
return [
'#theme' => 'article_card',
'#node' => $node,
'#title' => $node->getTitle(),
'#image' => $node->hasField('field_image')
? $view_builder->viewField($node->get('field_image'), 'card')
: NULL,
'#url' => $node->toUrl()->toString(),
'#cache' => [
'tags' => $node->getCacheTags(),
'contexts' => ['user.roles'],
],
];
}
}
Programmatic View Usage
use Drupal\views\Views;
// Get View programmatically and execute
$view = Views::getView('news_list');
$view->setDisplay('block_1');
$view->setArguments([42]); // contextual filter arguments
$view->setExposedInput(['category' => 'tech']);
$view->execute();
// Get results
$results = $view->result;
foreach ($results as $row) {
$node = $row->_entity;
// ...
}
// Render View block
$view->preExecute();
$view->execute();
$rendered = $view->buildRenderable('block_1');
Views and Custom Tables
If you need to show data from custom database table, register it for Views:
// my_module.views.inc
/**
* Implements hook_views_data().
*/
function my_module_views_data(): array {
$data = [];
$data['my_module_stats']['table']['group'] = t('My Module');
$data['my_module_stats']['table']['base'] = [
'field' => 'id',
'title' => t('My Module Stats'),
'help' => t('Statistics data from my module'),
];
$data['my_module_stats']['node_id'] = [
'title' => t('Node ID'),
'help' => t('Related node'),
'relationship' => [
'base' => 'node_field_data',
'base field' => 'nid',
'id' => 'standard',
'label' => t('Node'),
],
'filter' => ['id' => 'numeric'],
'sort' => ['id' => 'standard'],
'field' => ['id' => 'numeric'],
];
$data['my_module_stats']['view_count'] = [
'title' => t('View count'),
'field' => ['id' => 'numeric'],
'filter' => ['id' => 'numeric'],
'sort' => ['id' => 'standard'],
];
return $data;
}
REST Export Display
Views can export data to JSON via REST Export display:
/api/news.json?page=0&items_per_page=20&category=tech
Configured in UI or YAML. CORS headers configured in services.yml:
# web/sites/default/services.yml
cors.config:
enabled: true
allowedHeaders: ['*']
allowedMethods: []
allowedOrigins: ['https://frontend.example.com']
exposedHeaders: false
maxAge: false
supportsCredentials: false
Timelines
Simple View with exposed filters via UI + config export: half day. Custom plugin (row, filter, sort), custom data table, REST export: 2–3 days.







