Drupal 8 Views Field Handler Plugin — собственные поля для Views

Гайд о том как добавлять свои собственные поля для модуля Views.

26.07.2017
16 комментариев
29 мин.

Потихоньку начал разбирать Views API и буду публиковать гайды по различным его частям. В Drupal 7 мне всего пару раз довелось пописать плагины под Views и особого понимания там так и не получил, во многом, наверное, что материалы по которым я делал были очень и очень поверхностные. Даже сейчас, если погуглить по тем же полям Views гайды, везде очень поверхностно и как правило очень простенький пример в виде каркаса обьекта для поля. Поэтому, порывшись в исходниках, я попытаюсь расписать более подробно, и раскрыть другие части views в следующих статьях.

Большинство плагинов для Views объявляются по единому принципу и имеют очень похожие структуры и поведение, поэтому данный материал я постараюсь сделать чуть поподробнее, чтобы в дальнейших ссылаться на него.

Из чего состоит плагин Views Field Handler

Первым делом нужно хорошо понимать что такое Views и как он работает. Иметь хоть какой-то опыт кликанья там мышкой в каждом из его разделов. Думаю это не составит проблем, особенно в 8-ке где Views уже в ядре и он очень сильно задействован из коробки.

Затем, при помощи хука hook_views_data_alter() наше поле добавляется в качестве виртуального поля для Views, в котором мы указываем всю необходимую базовую информацию. Затем, мы уже объявляем плагин который полностью отвечает за получение данных, генерацию, настройки т.д.

И это всё. Да, так просто. Вот только этого недостаточно для полного понимания работы, особенно если требуется сделать поле посложнее.

Сам плагин поля является объектом который расширяет FieldPluginBase, а также может содержать следующие методы:

  • query(): метод для работы с запросом, про него ниже отдельный раздел.
  • defineOptions(): значения настроек по умолчанию. Используется если вы хотите сделать свою форму с настройками для поля. Должен возвращать массив. Значения настроек хранятся в $this->options в виде массива.
  • buildOptionsForm(&$form, FormStateInterface $form_state): форма с собственными настройками для вашего поля, используется обычный Form API.
  • submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []): метод вызываемый при субмите формы.
  • render(ResultRow $values): данный метод отвечает за вывод результата поля. Возвращает render array. Результатом может быть что угодно. Он принимает значения выполнения запроса и данные для конкретно текущего материала для которого рендирится поле. Если это view который работает с сущностями, то текущая сущность, для которой идет подготовка поля находится в $values->_entity, вы можете спокойно получать оттуда значения для своих нужд.
  • clickSortable(): данный метод отвечает за то, можно ли данное поле сортировать или же нет. Используется в различных типах вывода, например "таблица", где нажав на заголовок столбца происходит сортировка таблицы по возрастанию\убыванию. Вот это значение отвечает за то, можно ли будет по нему сортировать. Возвращает TRUE или FALSE. Данный метод переопределяет настройки по умолчанию, которые также можно задать в hook_views_data_alter(). Данное значение по умолчанию является TRUE. Единственное его применение в коде потребуется только тогда, когда возможность сортировать по полю является динамическим и зависит от каких-то настроек самого поля, во всех остальных случаях оно включено, либо отключено прямо в хуке.
  • clickSort($order): метод, который отвечает за сортировку, если данное поле является доступным для сортировки и используется формат вывода поддерживающий сортировку по значениям полей. В данном методе вам нужно добавить в $this->query необходимую сортировку. $order, который передается в метод содержит запрошенный вариант сортировки, может принимать лишь ASC и DESC. Данный метод должен быть объявлен обязательно, если clickSortable() может возвращать TRUE, или вы не переопределили в хуке на FALSE. Отсутствие данного метода приведет к ошибке при попытке отсортировать по значению данного поля.

Это далеко не все методы которые могут быть использованы и задействованы, но самые ходовые и необходимые. Остальные сильно специфические, и поняв как всё это работает на данных методах, вам не составит труда понять что делают другие, так как те что выше являются основными.

query() и $this->query

inb4 опять многА букАв

Данный метод почему-то в гайдах обходят стороной, все оставляют его пустым и пишут типа "нам не нужно корректировать запрос" и на этом всё. А что если нужно? Как тогда это делать? Там не \Drupal::database()->select() и оно работает несколько по другим правилам. Я считаю, что пропускать данный метод в гайдах про Views Field Handler просто безумие, учитывая что Views это такой построитель запросов, а поле зачастую является частью запроса, то не описать такую важную часть, как внедрение в этот самый запрос, совершенно неразумно, это отрезает от остального функционала данных полей. Да, сделать поле, которое выводит что-то очень просто, но когда требуется сделать какой-то доп. запрос или как-то вообще с ним поработать для поля, то не понимая как работает данный метод, это сделать невозможно. Я уже сам попался на такое в Drupal 7, банально потому что у меня не было полной картины и понимания как оно работает, так как и под 7-ку и под 8-ку все гайды просто описывают данный метод как незначительный, при всём при этом это самый важный метод! По сути данный метод и является самым движущим фактором для написания данной статьи. У андеада есть заметка на этот счет, но тогда, опять же, из-за того что я не видел всей картины, меня это не сдвинуло с мертвой точки. Вообще, вся сложность работы с Views API, по сути, это понять как оно работает под капотом, когда это понимание приходит, он становится не таким уж и страшным, а даже логичным.

Чтобы понять данный метод, нужно понимать что View работает с конкретной таблицой в БД. Например, выбрав при создании view "Содержимое", это означает что он будет работать с таблицой node и добавляя какой-либо Handler для views, буть то поле, сортировка, контекстуальный фильтр — они обязаны учитывать что они будут использоваться только в пределах определенного view и табилцы БД. Таким образом, данные полей получаются при помощи inner join запроса от этой таблицы, сортировка основывается опять же, на данных текущей таблицы или также inner join через связь в другую. Те же relationships, это опять же join по какому-то значению из БД текущей таблицы или ранее подключенных через связь. Это очень важно понимать, что запрос строится от основной таблицы, остальные подключаются только в качествве join. Views выполняет всего один запрос при генерации, и это тот самый запрос который хранится в $this->query и который вы также можете править через специальные для этого методы, главный из которых query().

Соответственно, если ваше поле требует получать какие-то данные из БД, вам нужно обращаться к query() методу. Тут, опять же, важно понимать что $this->query — свойство объекта, которое содержит итоговый объект запроса, общий для всех полей и Views в целом. Он содержит вообще всё что нужно и выполняется один раз, а не каждый раз на каждое поле. Каждый handler и плагин добавляет туда свои "требования" после чего он превращается в единый большой запрос к БД, а полученный результат Views отдает каждому полю, в зависимости от текущей обработки, а также затем это скармливает формату вывода, чтобы он уже с готовыми данными подготовил нужный вывод. Тут нельзя писать что хочешь. Если у вас не выходит через основной запрос получить нужные данные, то вы, конечно же, можете сделать обычный запрос к БД и получить нужные данные. Но, я не знаю верно это или нет, но что-то мне подсказывает что нет. И не забывайте, что вызовится он один раз. Вызов самостоятельных запросов в других методах, особенно в рендер методе, это явно проблема с логикей, с чем, как я упоминал выше, и столкнулся когда делал поля для 7-ки, все данные в идеале должны получаться при помощи одного единственного запроса.

Что можно сделать в данном методе? Подключить другие таблицы через связи, тем самым добавив поля (значения столбца из другой таблицы), добавить дополнительные condition к запросу (но лучше этого не делать тут) и многое другое. Тут можно делать вообще все что угодно с запросом, но делать этого не стоит. Опять же, надо четко понимать что за плагин вы делаете и что ему можно править. Никто не мешает в плагине для поля поправить в запросе кол-во результатов на страницу, или условия выборки (фильтрацию), но я думаю вы и сами понимаете, такое потом никто не найдет, это говнокод. Если хотите повлиять на сортировку значений (не поля, а именно результатов), надо делать плагин сортировки, если на выборку, то плагин фильтрации. Об этом я позже распишу, но в целом они все идентичные, просто на каждую задачу свой плагин, и не надо всё делать в одном плагине. Каждый решает свою задачу и никакую другую.

Что же мы можем тут использовать? Сразу пишу, это список полезных методов, не в контексте плагина поля, а вообще всех плагинов Views. Так вот, Views использует для построения запроса не стандартный \Drupal::database()->select(), а свой собственный построитель запроса Sql.php (core/modules/views/src/Plugin/views/query/Sql.php). Да, и даже такие плагины есть. Все его методы вы можете увидеть по ссылке, прямо на странице вбивайте Sql:: и смотрите, всё описывать не буду, опишу, на мой взгляд, самые ходовые и полезные:

addField()

Как понятно из названия, служит для добавления в запрос дополнительных значений (полей). Принимает следующие аргументы:

  • $table: название таблицы которая будет подключена при помощи inner join или NULL. В случае NULL подразумевается что поле является формулой. Например, какое-то простенькое математическое выражение на основе существующих полей. В случае если указали название таблицы, она обязана существовать, Views это проверит автоматически.
  • $field: название поля которое будет добавлено (столбца из таблицы указнной выше). Если в первом аргументе указано NULL, то тут должна быть формула.
  • $alias: синоним для значения, который будет использоваться в дальнейшем. Например, если вы передадите первые два параметра node__body и body_summary соответственно, то указав синоним body, значение будет доступно по данному синониму. Это делается на уровне SQL запроса в том числе, например, текущий пример будет построен как node__body.body_summary AS body. Вы можете использовать данный синоним для дальнейших нужд при запросе, например при добавлении поля формулы вы уже сможете ссылаться на значение при помощи синонима. Автоматически Views дает синоним по следующему шаблону $table_$field. Вы можете передать NULL чтобы отключить присвоение синонима.
  • $params: массив с дополнительными параметрами. Может принимать function который может содержать функцию для аггрегации, например SUM, а также aggregate, который в случае TRUE пометит поле что оно требует аггрегации. Используется самим Views, самим это использовать нет никакого смысла, никакого эффекта не даст. Так что этот параметр можно всегда смело опускать.

Данный метод возвращает строку с синонимом поля. Если потрбуется, можете принимать, но как правило нет смысла.

Собственно данный метод служит для добавления каких-либо данных к текущему запросу. Он самый полезный при работе с полями. Вот несколько примеров:

Примеры $this->query->addField()
// Assuming your node entity has fields 'field_example_first` and 'field_example_second' as
// integers.

// Add first field.
$this->query->addField('node__field_example_first', 'field_example_first_value', 'first');
// Add second field.
$this->query->addField('node__field_example_second', 'field_example_second_value', 'second');
// Calculate SUM of this two fields. You CAN'T use aliases here.
$this->query->addField(NULL, 'node__field_example_first.field_example_first_value + node__field_example_second.field_example_second_value', 'first_second_sum');

Значения полей подключаются при помощи inner join запроса, и если вы знаете что это такое и как оно работает, у вас должен был появиться вопрос, а как же связь? Связи устанавливаются автоматически на основе объявления данного типа данных для Views. Например у node связь идет через entity_id у таблицы которая подключается. Так что просто так не подключить что попало, подключаемая табилца, для node должна содержать столбец с entity_id, который и будет задействован для связи и получения значения. Например, для сущности File, связующим столбцом будет fid. Всё это можно посмотреть у тех модулей, что объявляют таблицы для Views в файле MODULENAME.views.inc в хуке file_field_views_data (см. base field). Вообще, это касается по сути только кастомных таблиц, для остальных сущностей интеграция происходит автоматическая, и надо смотреть в объявление сущности на их entity key id (src/Entity/EntityName.php модуля где объявляется сущность). Тут могу немного ошибаться, если дальше в раскопках обнаружу иное поведение, заменю. Или можете в комментах меня поправить.

Condition()

Это не совсем метод, но это очень важный объект для дальнейшего описания методов. Позволяет создать условие для запроса, в виде объекта, для дальнейшего использования его в методах. Сначала объявляется Condition() нужного типа и записывется в переменную, затем добавляются условия, и данная переменная передается методам которые это поддерживают. Принимает различные операторы для сравнения основнывая на SQL запросах.

Пример Condition()
$or = new Condition('OR');
$or->condition('field_name', 'VALUE');
$or->condition('field_name2', 'VALUE', '<>');
$or->condition('field_name2', 'VALUE', 'IS NULL');
$this->query->addWhere(0, $or)

Это замена db_or() и подобным функциям из 7-ки.

addWhere()

Данный метод служит для добавления условий выборки запросу. Принимает следующие аргументы:

  • $group: название группы для которой будет использовано условие. Если указанной группы не существует, то она будет создана, если же не нужно вообще, указывается 0 (общая, по умолчанию).
  • $field: название поля по которому будет проводиться проверка. В данный аргумент можно передавать DatabaseCondition объект (см. предыдущий пример Condition()).
  • $value = NULL: значение по которому будет производиться условие. Может быть как простым значением на проверку, так и массивом с данными, где каждый элемент массива будет зависеть от $operator.
  • $operator = NULL: оператор сровнения SQL. Например: =, <, >=, IN, LIKE, BETWEEN и т.д. По умолчанию, если передан NULL, будет использоваться сравнение =.
Пример addWhere()
// Aliases doesn't supported here.
// WHERE (node__field_example_first.field_example_first_value = '2')
$this->query->addWhere(0, 'node__field_example_first.field_example_first_value', 2);
// WHERE (node__field_example_first.field_example_first_value IN ('1', '2', '3'))
$this->query->addWhere(0, 'node__field_example_first.field_example_first_value', ['1', '2', '3'], 'IN');
// WHERE ((node__field_first_number.field_first_number_value IN ('1', '2', '3')) AND (node__field_first_number.field_first_number_value LIKE 'test' ESCAPE '\\')) … else
$this->query->addWhere('my_group', 'node__field_first_number.field_first_number_value', ['1', '2', '3'], 'IN');
$this->query->addWhere('my_group', 'node__field_first_number.field_first_number_value', 'test', 'LIKE');

addOrderBy()

А данный метод ответчает за сортировку (ORDER BY) результатов запроса. Данный метод можно использовать в Views полях в определенном случае (clickSort()), для этого будет отдельный пример.

Принимает следующие аргументы:

  • $table: название таблицы по которой будет производиться сортировка. Если сортировка должны быть по какой-то формуле, то указывается NULL.
  • $field = NULL: поле (столбец таблицы), по которому будет производиться сортировка, если нужна формула, как и выше, указывается NULL.
  • $order = 'ASC': вид сортировки, либо ASC — по возрастанию, либо DESC — по убыванию.
  • $alias = '': синоним для поле, по которому будет производиться сортировка. Аналогично из addField(). Если первые два аргумента переданы как NULL, тут можно указать уже существующий синоним, и по нему будет произведена сортировка.
  • $params = []: дополнительные параметры при добавления поля через addField() (если указаны первые два).
Пример addOrderBy()
$this->query->addOrderBy('node__field_example_first', 'field_first_number_value', 'ASC', 'first');
// If field is already added 
$this->query->addField('node__field_example_second', 'field_example_second_value', 'second');
$this->query->addOrderBy(NULL, NULL, 'DESC', 'second');

addTag()

Это очень простенький метод, я даже примеров приводить не буду. В него передается лишь один аргумент с названием тега. Данный тег будет добавлен запросу, в дальнейшем, чтобы в логах запросов вы могли найти запросы которые помечены им, и решить какие-то проблемы, если такие возникли.

$this->ensureMyTable()

Если вы собираетесь использовать query() метод, в начале принято писать $this->ensureMyTable(), так как он проверяет, добавлена ли основная таблица по которой будет проходить запрос, непосредственно в сам запрос, во избежание проблем с генераций самого запроса. Без него тоже будет работать, но возможно при каких-то обстоятельствах произойдет сбой. Так что не поленитесь написать :)

Я задел все основные методы для построения запроса Views. Если вы их поняли, и поняли что Views работает на одном запросе, то считайте вы уже поняли как делать все плагины, и учитывать особенности Views с его запросом. Это самая сложная часть для понимания, серьезно. Она совершенно не очевидна без пинка или если не копать в сам Views. Надеюсь это вам хоть как-то облегчит понимание того что происходит под капотом у Views.

Возвращаемся непосредственно к нашему главному топику, о том как добавлять поля. Так как всё уже разжевано, и то как создается, и то как можно подлезать к запросу, осталось только набросать примеров.

Пример №1

Данный пример будет очень простой, примерно то что все и показывают в своих гайдах. В нём мы добавим к таблице node поле "Относительная дата создания", которое будет выводить дату создания по принципу: сегодня, вчера, позавчера, 3 дня назад, …, 7 дней назад, d.m.Y. Иными словами, дата будет выводиться текстом, а спустя 7 дней, на 8-ой, становиться обычной в нужном формате. В данном случае нам даже не придется влезать в запрос, так как дата создания материала автоматически добавляется для node самим Views.

Первым делом нам нужно объявить наше поле для Views. Так как мы добавляем свое "поле" в чужую БД, мы должны добавлять информацию о нем через alter. Для этого используется hook_views_data_alter(). Данный хук в 8-ке нужно писать в MYMODULE.views.inc. Он также будет работать и в обычном MYMODULE.module, но это своего рода такой стандарт в 8-ке, чтобы четко видеть что в конкретном модуле есть какая-то интеграция с Views.

Добавляем наше поле:

dummy.views.inc
<?php

/**
 * @file
 * Views hooks.
 */

/**
 * Implements hook_views_data_alter().
 *
 * Alter DB tables defined via hook_views_data().
 */
function dummy_views_data_alter(array &$data) {
  $data['node']['dummy_created_relative'] = [
    'title' => t('Relative creation date'),
    'field' => [
      'title' => t('Relative creation date'),
      'help' => t('The creation date will be printed as relative date.'),
      'id' => 'dummy_created_relative',
    ],
  ];
}

Бегло пробегусь по хуку, так как это первый пример.

$data содержит информацию о всех таблицах подключенных к Views через hook_views_data. Вы можете добавлять в каждую из них свои поля, например циклом, или в какие-то конкретные, как в данном примере node. Первый ключ — название таблицы, воторой — название поля. Будьте аккуратны, там уже есть другие данные и также добавленные поля, не поломайте случайно ничего. Для безопасности, так как вы лезете в чужую таблицу, лучше добавлять названию поля также название модуля, чтобы наверняка.

Дальше задается масив с данными. В нашем случае поле, и нам нужно указать:

  • title: заголовок для нашего поля что мы объявляем. Не могу сказать где точно данный заголовок используется, я не обнаружил. Возможно чтобы в альтере другие поняли что к чему.
  • field: массив с информацией о новом поле для указанной таблицы:
    • title: название поля, будет отображаться во Views UI.
    • help: описание поля, будет отображаться во Views UI.
    • id: машинное название поля, по нему Views будет искать Field Handler Plugin.
    • click sortable: можно установить FALSE если сортировку по полю надо жестко отключить. Далее нам уже нужно объвлять сам плагин поля. Плагины Views находятся по пути src/Plugin/views, далее, там каждый тип плагина имеет свою папку. Для полей это очевидный field. Называть объект для поля, лучше всего как и само машинное поле, только трансформировав его в CamelCase. В итоге, для нашего примера нужно создать DummyCreatedRelative.php.

Мы также добавим настройки для нашего поля, мы дадим пользователям возможность задавать формат даты, которая будет выводиться спустя 7 дней, а по умолчанию она будет d.m.Y.

Вот собственно и код поля:

src/Plugin/views/field/DummyCreatedRelative.php
<?php

namespace Drupal\dummy\Plugin\views\field;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * @ingroup views_field_handlers
 *
 * @ViewsField("dummy_created_relative")
 */
class DummyCreatedRelative extends FieldPluginBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    // We don't need to modify query for this particular example.
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();
    $options['relative_date_format'] = ['default' => 'd.m.Y'];
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    $form['relative_date_format'] = [
      '#type' => 'textfield',
      '#required' => TRUE,
      '#title' => $this->t('Relative date format'),
      '#description' => $this->t('This format will be used for dates older than 7 days.'),
      '#default_value' => $this->options['relative_date_format'],
    ];
    parent::buildOptionsForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function render(ResultRow $values) {
    $created = $values->node_field_data_created;
    // Default result if conditions will fail.
    $pub_date = date($this->options['relative_date_format'], $created);
    if ($created >= strtotime('today')) {
      $pub_date = $this->t('today');
    }
    else if ($created >= strtotime('-1 day')) {
      $pub_date = $this->t('tomorrow');
    }
    else if ($created >= strtotime('-2 days')) {
      $pub_date = $this->t('day before yesterday');
    }
    else if ($created >= strtotime('-3 days')) {
      $pub_date = \Drupal::translation()->formatPlural(3, '@count day ago', '@count days ago', ['@count' => 3]);
    }
    else if ($created >= strtotime('-4 days')) {
      $pub_date = \Drupal::translation()->formatPlural(4, '@count day ago', '@count days ago', ['@count' => 4]);
    }
    else if ($created >= strtotime('-5 days')) {
      $pub_date = \Drupal::translation()->formatPlural(5, '@count day ago', '@count days ago', ['@count' => 5]);
    }
    else if ($created >= strtotime('-6 days')) {
      $pub_date = \Drupal::translation()->formatPlural(6, '@count day ago', '@count days ago', ['@count' => 6]);
    }
    else if ($created >= strtotime('-7 days')) {
      $pub_date = \Drupal::translation()->formatPlural(7, '@count day ago', '@count days ago', ['@count' => 7]);
    }

    return [
      '#markup' => $pub_date,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function clickSort($order) {
    $this->query->addOrderBy('node_field_data', 'created', $order);
  }

}

Тут должно быть всё ясно, так как каждый метод описан отдельно. Единственное на что я обращу внимание, то что тут отсутствует submitOptionsForm(). Настройки сохраняются автоматически если их названия сходятся. Этот метод нужен только для более сложных структур, а также обработки значений из формы перед сохранением, если это требуется.

Сбросив кэш, данное поле станет доступно для использования и добавив его, оно начнет выводить свои значения.

Наше поле
Настройки поля
Результат

Пример №2

В данном примере мы добавим поле которое будет производить простые арифметические выражения между двумя полями сущности. Мы дадим пользователю возможность выбирать необходимые два поля и математическую операцию которая будет выполнена. Всё очень просто, возможно бесполезно, но тут какраз в дело пойдет query(), так как поля заранее не известны нам, и нам нужно будет динамически данные поля добавлять в запрос.

Математическую операцию мы будем проводить в render(), хотя это возможно сделать прямо в запросе, так как они простые и общие для всех. Поэтому, так как результат не будет доступен в запросе, мы отключим сортировку по данному полю, заодно и будет видно как можно отключить её без метода.

На выбор пользователю мы будем предоставлять только поля из сущности node, и только типов integer, float, decimal.

Собственно начнем как и раньше, с добавления инфомрации о поле в hook_views_data_alter():

dummy.views.inc — dummy_views_data_alter()
$data['node']['dummy_two_fields'] = [
  'title' => t('Two fields math operation'),
  'field' => [
    'title' => t('Two fields math operation'),
    'help' => t('Simple math operation between two selected fields.'),
    'id' => 'dummy_two_field_math',
    'click sortable' => FALSE,
  ],
];

Ну и сам код поля:

src/Plugin/views/field/DummyTwoFieldMath.php
<?php

namespace Drupal\dummy\Plugin\views\field;

use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * @ingroup views_field_handlers
 *
 * @ViewsField("dummy_two_field_math")
 */
class DummyTwoFieldMath extends FieldPluginBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $this->ensureMyTable();
    $first_field = $this->options['first_field'];
    $second_field = $this->options['second_field'];
    if ($first_field && $second_field) {
      $table = $this->table;
      $first_field_db_table = $table . '__' . $first_field;
      $second_field_db_table = $table . '__' . $second_field;
      $this->query->addField($first_field_db_table, $first_field . '_value', 'dummy_first_field');
      $this->query->addField($second_field_db_table, $second_field . '_value', 'dummy_second_field');
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();
    $options['first_field'] = ['default' => NULL];
    $options['second_field'] = ['default' => NULL];
    $options['math_operation'] = ['default' => '+'];
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    $field_options = $this->getFieldOptions();
    $form['first_field'] = [
      '#type' => 'select',
      '#title' => $this->t('First field'),
      '#required' => TRUE,
      '#options' => $field_options,
      '#default_value' => $this->options['first_field'],
    ];
    $form['second_field'] = [
      '#type' => 'select',
      '#title' => $this->t('Second field'),
      '#required' => TRUE,
      '#options' => $field_options,
      '#default_value' => $this->options['second_field'],
    ];
    $form['math_operation'] = [
      '#type' => 'select',
      '#title' => $this->t('Math operation'),
      '#required' => TRUE,
      '#options' => [
        '+' => '+',
        '-' => '-',
        '*' => '*',
        '/' => '/',
      ],
      '#default_value' => $this->options['math_operation'],
    ];
    parent::buildOptionsForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function render(ResultRow $values) {
    if (isset($values->dummy_first_field) && isset($values->dummy_second_field)) {
      $result = NULL;
      switch ($this->options['math_operation']) {
        case '+':
          $result = $values->dummy_first_field + $values->dummy_second_field;
          break;

        case '-':
          $result = $values->dummy_first_field - $values->dummy_second_field;
          break;

        case '*':
          $result = $values->dummy_first_field * $values->dummy_second_field;
          break;

        case '/':
          $result = $values->dummy_first_field / $values->dummy_second_field;
          break;
      }

      return [
        '#markup' => $result,
      ];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getFieldOptions() {
    $allowed_type_list = ['integer', 'float', 'decimal'];
    $exclude_fields = ['nid', 'vid'];
    $field_map = \Drupal::service('entity_field.manager')->getFieldMap();
    $options = [];
    foreach ($field_map['node'] as $field_name => $field_info) {
      if (in_array($field_info['type'], $allowed_type_list) && !in_array($field_name, $exclude_fields)) {
        $options[$field_name] = $field_name;
      }
    }
    return $options;
  }

}

После этого сбрасываем кэш, и наслаждаемся!

Настройки поля
Результат вывода материалов с данным полем

На этом думаю хватит. Принцип работы полей и Views с его запросом должен стать куда яснее. Далее, чуть позже, я понапишу гайды про остальные хендлеры. Если вам прямо нетерпится и их попробовать, принцип у них одинаковый, даже объявляются одинакого, только вместо field везде используется нужный тип плагина. Их особенности можно всегда разрыть в ядре у самого views.

P.s.

И возможно, у вас когда-нибудь назреет вопрос как был в своё время у меня и из-за чего я жестко забуксовал с Views. Допустим вы сделали поле которое по какой-то сложной формуле считает что-либо, например рейтинг пользователя, вы сделали поле, всё выводится, всё круто, но вот решили сделать сортировку по данному полю. Если сортировать нужно все результаты, а не в таблице, то во-первых, нужно писать sort плагин, а во-вторых, результат подсчета придется где-то хранить в БД. Т.е. нужно создавать новую таблицу, и там хранить результат рассчетов с проставлением связей с нужными материалами чтобы Views смог заджойнить данные таблицы. И только тогда вы сможете сделать сортировку по данному значению.

Прикрепленные файлы
Готовый модуль с примерами — dummy.tar.gz, 2 КБ
Drupal
Drupal 8
Views
Plugin API

Комментарии

Владимир   сб, 14/10/2017 - 21:09

Спасибо, статья хорошо помогла разобраться со Views.

Ivan   пн, 27/08/2018 - 12:38

Спасибо за статью, очень полезно

Есть возможность как-то подключить в public function query() свою кастомную таблицу из базы данных? Например есть таблица в бд, my_custom_data, в ней есть поля entity_id и custom_data. Можно как-то получить custom_data по id ?

Niklan   пн, 27/08/2018 - 13:08

Если не ошибаюсь, там есть методы leftJoin, rightJoin. По ним, имея данные с основной табилцы можно получать из другой. Иначе никак. Только где-то дальше, что некорректно уже. Сейчас под рукой не на чем проверить. В общем джоинить там должно быть можно, просто подключать доп. таблицы - нет.

Ivan   пн, 27/08/2018 - 14:45

Спасибо, разобрался) использовал addRelationship() вместе с left join

Erbie   вт, 25/09/2018 - 13:19

Можно ли переопределить query для существующего фильтра для определенного поля?

Ким Чен Ин   пт, 26/10/2018 - 00:19

Можно вот тут попоробней "Называть объект для поля, лучше всего как и само машинное поле, только трансформировав его в CamelCase" "лучше всего как и само машинное поле" - то есть можно и подругому? Никак не пойму как Views определяет Field Handler Plugin который принадлежит именно этому полю.

Niklan   пт, 26/10/2018 - 08:31

Можно и без CamelCase, плагинам все равно на название файла. Просто правильнее CamelCase.

Никак не пойму как Views определяет Field Handler Plugin который принадлежит именно этому полю.

По аннотации @ViewsField("dummy_created_relative"), которая имеет тоже название что и добавляется в хуке.

Ким Чен Ин   пт, 26/10/2018 - 13:05

Вот теперь понятно, а то всю голову сломал, изучая модуль dc_ajax_add_cart_views, спасибо.

Павел Седой Джун   вс, 13/10/2019 - 17:27

Никита, спасибо Вам!) Очень помогаете. В "Готовый модуль с примерами" закралась опечатка: в DummyTwoFieldMath.php надо исправить плюс на минус в строке 90. Там сейчас так:
(line 89) case '-': (line 90) $result = $values->dummy_first_field + $values->dummy_second_field;

Alex   пт, 24/07/2020 - 12:07

Подскажите что с сортировкой в таблице по кастомному полю?

wolf   ср, 21/07/2021 - 14:24

В примере кода, где идут математические действия (в статье) при выволнении минуса установлен "+"