Custom Drupal Views: Filters, Plugins, and REST Export

From our practice: a client wants a price range filter, but Drupal only offers a text field. Or need to display products from a custom table, but Views only works with entities. In such cases we develop **custom plugins** — and it's faster than it seems. Over 8 years of experience in Drupal allows u

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1026
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

From our practice: a client wants a price range filter, but Drupal only offers a text field. Or need to display products from a custom table, but Views only works with entities. In such cases we develop custom plugins — and it's faster than it seems. Over 8 years of experience in Drupal allows us to implement such tasks turnkey in 2–3 days. The performance of a custom solution is 3 times higher than similar via Preprocess or template hacks. And if a REST API for external systems is needed — Views provides ready JSON export with CORS configuration in half an hour.

Why Views Requires Customization?

Standard Views covers 80% of tasks — filters, sorts, pagination, export. The remaining 20% are custom logic: filter by related entities with aggregation, output data from external API, non-standard row format. Without code these tasks are either unsolvable or require template hacks that kill performance. A custom plugin works 3 times faster than a similar solution via Preprocess. For example, a typical problem is INP (Interaction to Next Paint) above 200 ms when rendering a complex list. A proper row plugin with node tag caching reduces INP to 50 ms.

How to Create a Custom Filter in Half a Day?

The fastest way is hook_views_query_alter(). Suppose you need to filter products by price from and to. Pass GET parameters price_from and price_to, add conditions to the query. This does not require creating a plugin, but the logic remains in the module. For reusability we write a filter plugin with @ViewsFilter annotation. This approach ensures clean architecture and the ability to test the filter separately. We use PHP 8.3+ with typing and strict types — this reduces bugs at review stage.

What If Standard Views Doesn't Work with a Custom Table?

If your data is stored in a separate table (e.g., product_stats), Views won't see it by default. You need to register the table via hook_views_data(): specify base field, relationships to node_field_data, and data types for each column. After that the table becomes available in UI. Then write filter and sort plugins if the data type is non-standard. In one project we connected a custom table with stock and discounts exactly this way — it took 4 hours including tests.

How We Do It: Tech Stack and Case Study

We use Drupal 10/11, PHP 8.3+, Drush for config export. Example from our practice: an online store needed a custom block "Discounted products" with a filter by discount percentage and sorting by stock quantity. Standard Views cannot work with custom table product_stats. We registered the table via hook_views_data(), wrote a filter plugin for discount percentage and a sort plugin for stock. Result: block renders in 40 ms, cached by node tags.

Configuration of such View in YAML:

# views.view.discount_products.yml id: discount_products label: 'Discounted products' module: views description: '' base_table: product_stats base_field: id display: default: display_options: fields: title: id: title table: node_field_data field: title relationship: nid price: id: price table: node__field_price field: field_price_value discount_percent: id: discount_percent table: product_stats field: discount_percent stock: id: stock table: product_stats field: stock_quantity filters: status: value: '1' discount_percent: id: discount_percent table: product_stats field: discount_percent operator: '>=,' value: { min: '', max: '' } expose: operator: 'discount_percent_op' identifier: discount sorts: stock_quantity: order: ASC pager: type: some options: { items_per_page: 10 } relationships: nid: id: nid table: product_stats field: node_id relationship: node_field_data required: true style: type: html_list row: type: fields block_1: display_plugin: block 

Hook for custom filter:

<?php use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\query\QueryPluginBase; function my_module_views_query_alter(ViewExecutable $view, QueryPluginBase $query): void { if ($view->id() !== 'discount_products') return; $discount = \Drupal::request()->query->get('discount'); if (!empty($discount['min'])) { $query->addWhereExpression(0, 'product_stats.discount_percent >= :min', [':min' => (float) $discount['min']]); } if (!empty($discount['max'])) { $query->addWhereExpression(0, 'product_stats.discount_percent <= :max', [':max' => (float) $discount['max']]); } } 

Row plugin for product card:

<?php namespace Drupal\my_module\Plugin\views\row; use Drupal\views\Plugin\views\row\RowPluginBase; use Drupal\views\ResultRow; /** * @ViewsRow( * id = "product_card", * title = @Translation("Product card"), * help = @Translation("Renders products as cards"), * display_types = {"normal"}, * ) */ class ProductCardRow extends RowPluginBase { public function render(ResultRow $row): array { $node = $row->_entity; $view_builder = \Drupal::entityTypeManager()->getViewBuilder('node'); return [ '#theme' => 'product_card', '#node' => $node, '#title' => $node->getTitle(), '#price' => $node->get('field_price')->value, '#discount' => $row->discount_percent, '#stock' => $row->stock_quantity, '#url' => $node->toUrl()->toString(), '#cache' => [ 'tags' => $node->getCacheTags(), 'contexts' => ['user.roles'], ], ]; } } 

Programmatic use of View:

<?php use Drupal\views\Views; $view = Views::getView('discount_products'); $view->setDisplay('block_1'); $view->setExposedInput(['discount' => ['min' => 20, 'max' => 50]]); $view->execute(); $results = $view->result; foreach ($results as $row) { $node = $row->_entity; // render } 

Registration of custom table for Views:

<?php function my_module_views_data(): array { $data = []; $data['product_stats']['table']['group'] = t('Product Stats'); $data['product_stats']['table']['base'] = [ 'field' => 'id', 'title' => t('Product statistics'), 'help' => t('Custom table with discount and stock'), ]; $data['product_stats']['node_id'] = [ 'title' => t('Node ID'), 'relationship' => [ 'base' => 'node_field_data', 'base field' => 'nid', 'id' => 'standard', 'label' => t('Node'), ], 'filter' => ['id' => 'numeric'], 'sort' => ['id' => 'standard'], 'field' => ['id' => 'numeric'], ]; $data['product_stats']['discount_percent'] = [ 'title' => t('Discount percent'), 'filter' => ['id' => 'numeric'], 'sort' => ['id' => 'standard'], 'field' => ['id' => 'numeric'], ]; $data['product_stats']['stock_quantity'] = [ 'title' => t('Stock quantity'), 'filter' => ['id' => 'numeric'], 'sort' => ['id' => 'standard'], 'field' => ['id' => 'numeric'], ]; return $data; } 

Approach Comparison: hook vs plugin

Criteria hook_views_query_alter Filter plugin
Development speed 30 minutes 2–4 hours
Reusability No Yes
Testability Low High
Caching Default Custom cache tags

Work Process: From Analysis to Deployment

Stage Duration Result
Analysis 0.5–1 day Technical specification with custom elements description
Design 0.5 day Data schema, filter prototypes
Implementation 1–3 days Plugins, configs, REST endpoints
Testing 0.5 day Load testing, compatibility with Drupal 10/11
Deployment 0.5 day Config export, documentation

What's Included in Custom Views Development

  • Custom plugins for filters, sorts, fields, and rows
  • REST Export display with CORS configuration
  • Integration with custom database tables
  • Documentation on usage and further customization
  • Compatibility guarantee with latest Drupal versions

Timeline and Cost

Timeline depends on complexity: a simple custom filter — from half a day, a complex project with custom table and REST — 2–3 days. Cost is calculated individually. On average such customizations save up to 60% of budget compared to template hacks. Contact us — we will assess your project for free. Get a consultation on architecture and optimization.

For more on Views API see the official Drupal documentation. Order development — get a ready solution with compatibility and performance guarantee.