Как подружить свой тип данных с Views

При разработке модулей для Drupal мы частенько создаем программно новые типы материалов (то есть нод). В процессе иногда возникает соблазн быстренько запрограммировать вывод какой-нибудь отсортированной коллекции этих материалов. Например, вывод нод-городов, отсортированных по алфавиту, или по древности, или сгруппированных по странам. Таких полезных коллекций может образоваться достаточно много и программно создавать каждую из них — не очень эффективно. Особенно если впоследствии нам захочется что-то поменять, объединить, или переделать. Гораздо аккуратнее — использовать модуль Views.

Вообще говоря, Views — один из краеугольных камней разработки на Drupal. Хотите помесячный архив нод? Views. Хотите вывести только заголовки и даты нод? Views. Хотите вывести ноды, сгруппированные по именам авторов? Views. Хотите получить список IP, с которых пользователь Василий заходил на сайт в течение Великого поста? Тоже Views. Один из самых популярных ответов на вопрос «Как в Друпале сделать…» — Views. Так что, чем быстрее разработчик подружится с этим прекрасным модулем — тем лучше. Для этого, кстати, есть неплохой мануал на английском языке: http://views-help.doc.logrus.com/.

Однако как только разработчик захочет получить с помощью Views коллекцию созданных им данных — например, из самодельной таблицы, сделанной для нового типа материала — он обнаружит, что Views еще не знает о его таблице. Так вот, раз и навсегда рассказать о своей таблице модулю Views — на мой взгляд, гораздо более правильное решение, чем программировать вручную все необходимые представления.

Далее — о том, как это сделать.

Предполагаю, что читатель уже умеет программно создавать свои типы материалов. Рассказывать буду на примере модуля, в котором новый тип уже создан. А чтобы не публиковать код модуля с описанием нового типа, предлагаю просто его скачать:
Скачать модуль mytest.
В модуле уже реализовано все, о чем дальше пойдет речь в заметке.

Итак, у нас есть тип материала testnode, в который мы добавили два целочисленных поля — int_count и int_total, хранящиеся в таблице БД {testnode}. Научим Views загружать эти поля из базы данных, выводить их, а также сортировать и фильтровать по ним коллекцию.

Перво-наперво, доложим Views о том, какую версию API мы используем (а используем мы версию Views 2.8 с Drupal 6.x). Для этого добавим в наш файл mytest.module вызов хука hook_views_api:

  1. function mytest_views_api() {
  2.   return array('api' => 2);
  3. } //mytest_views_api

Затем создадим файл mytest.views.inc, в котором модуль Views самостоятельно сможет найти вызов нужных хуков. Пришла пора рассказать Views про нашу таблицу. Для этого используется хук hook_views_data(), возвращающий массив с информацией об описываемых таблицах и их полях. Реализация хука должна возвращать ассоциативный массив формата $data['имя_таблицы']['имена_полей']. Также в массиве для каждой таблицы может содержаться подмассив $data['имя_таблицы']['table'] с общей информацией о таблице. Рассмотрим хук по порядку:

  1. function mytest_views_data() {
  2.   $data = array();
  3.  
  4.   $data['testnode'] = array(
  5.     'table' => array(
  6.       'group' => t('Testnode'),
  7.     ),
  8.     //...

Подмассив ['table'] содержит основную информацию о таблице. Элемент ['group'] — это имя группы, в которой объявляемые нами поля появятся в выпадающих списках Views. В принципе, можно указывать имя группы для каждого описываемого поля, но если этого не делать — имя группы унаследуется из секции ['table'].

Прежде чем перечислять поля таблицы {testnode}, небольшое лирическое отступление о Views. Что, собственно, делает модуль Views, когда пользователь указывает в его удобных, хотя и запутанных на первый взгляд колонках интерфейса аргументы, фильтры, поля, отношения и способы сортировки? Правильно, модуль Views по заданным настройкам составляет SQL-запрос, вытягивающий из базы данных те данные, которые пользователь указал, и сортирует их так, как пользователю надо. Так что, чтобы пользователь мог указывать во Views поля таблицы {testnode}, нужно рассказать модулю Views, что он должен вставлять в SQL-запрос.

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

  • ['title'] — заголовок поля, таким он будет показан пользователю в интерфейсе Views;
  • ['group'] — если хочется, можем указать для поля группу, отличную от указанной в ['table'];
  • ['help'] — здесь можно задать текст подсказки по данному полю, который пользователь увидит мелким шрифтом рядом с заголовком;
  • ['real field'] — если в массиве $$data мы в качестве ключа указали для данного поля синоним, то в ['real field'] нужно привести настоящее название поля в таблице БД.

Перечисленные элементы — заголовок, помощь, группа — имеют скорее декоративное назначение. Кроме них нам нужно заполнить элементы массива, имеющие непосредственное отношение к логике работы Views. Это элементы ['field'], ['filter'], ['sort'], ['argument'] и ['relationship'], отвечающие соответственно за чтение поля из БД, фильтрацию поля (с помощью WHERE), сортировку поля (ORDER BY), использование поля в качестве аргумента (в URL) и отношения (то есть присоединение другой таблицы через данное поле). Все пять элементов должны быть ассоциативными массивами. В них нужно обязательно привести значение элемента ['handler'], а также, по необходимости, дополнительные элементы, которые могут варьироваться в зависимости от значения ['handler'].

Здесь нужно еще одно лирическое отступление. В отличие от ядра Друпала и большинства сторонних модулей, API модуля Views — объектно-ориентированный (насколько это возможно). Товарищей, привыкших программировать под Друпал, Views API может несколько смутить, но смею заверить: это плюс, а не минус. Кстати, по поводу отсутствия объектно-ориентированного подхода в «капельке» разные граждане периодически поднимают бучу. Автор статьи, однако, относится к этой особенности безразлично и принимает ее как данность, с которой нужно работать, а не хаять почем зря.

Рассмотрение объектно-ориентированной модели Views API, в принципе, должно быть темой отдельной большой статьи. Поэтому здесь коснемся только того, что нас непосредственно волнует, то есть хэндлеров. Понимающий английское письмо читатель может ознакомиться с подробным описанием Views API на сайте: http://views2.logrus.com/doc/html/index.html.

Корнем модели является класс views_object, а уже от него наследуется класс views_handler. Ветка производных от views_handler классов управляет поведением заданных пользователем элементов представления (аргументов, полей, фильтров и так далее). То есть хэндлеры — это управляющие классы, задача которых следить, чтобы все что нужно было добавлено в запрос, чтобы из БД был получен нужный результат, чтобы был сгенерирован правильный текст поля и так далее. Таким образом, чтобы пользователь мог использовать поля из таблицы {testnode} для генерации своих представлений, нужно указать для каждого поля правильные классы-хэндлеры.

Хэндлеры бывают очень разные в зависимости от типа данных, необходимого эффекта, оказываемого ими на запрос, сложности фильтра, который мы хотим использовать и тому подобное. Для нашего тестового модуля нам понадобятся хэндлеры views_handler_field (для вставки в запрос обычного поля), views_handler_filter_numeric (чтобы в фильтре можно было пользоваться значением поля как числом) и views_handler_sort (обычный хэндлер для сортировки ASC/DESC без извратов). В результате поля таблицы {testnode} мы опишем так:

  1.     //... это продолжение хука mytest_views_data
  2.     'tid' => array(
  3.       'title' => t('Test Node ID'),
  4.       'field' => array(
  5.         'handler' => 'views_handler_field',
  6.       ),
  7.       'filter' => array(
  8.         'handler' => 'views_handler_filter_numeric',
  9.       ),
  10.     ),
  11.     'nid' => array(
  12.       'title' => t('Node ID'),
  13.       'field' => array(
  14.         'handler' => 'views_handler_field',
  15.       ),
  16.       'filter' => array(
  17.         'handler' => 'views_handler_filter_numeric',
  18.       ),
  19.     ),
  20.     'int_count' => array(
  21.       'title' => t('Count'),
  22.       'field' => array(
  23.         'handler' => 'views_handler_field',
  24.       ),
  25.       'sort' => array(
  26.         'handler' => 'views_handler_sort',
  27.       ),
  28.       'filter' => array(
  29.         'handler' => 'views_handler_filter_numeric',
  30.       ),
  31.     ),
  32.     'int_total' => array(
  33.       'title' => t('Total'),
  34.       'field' => array(
  35.         'handler' => 'views_handler_field',
  36.       ),
  37.       'sort' => array(
  38.         'handler' => 'views_handler_sort',
  39.       ),
  40.       'filter' => array(
  41.         'handler' => 'views_handler_filter_numeric',
  42.       ),
  43.     ),
  44.   );

Теперь необходимо снова вернуться к секции ['table'] массива $data['testnode']. Помимо имени группы, в ней содержится важная информация о том, базовая ли это таблица, или ее нужно присоединять к другой при генерации представления. Базовая таблица — это таблица, центральная для Views, как например таблица node для Node view или таблица user — для User view. Будь наша таблица базовой, мы задали бы для нее в массиве $data['имя_таблицы']['table']['base'] элементы ['field'] (название поля-первичного ключа), ['title'] (заголовок таблицы), ['help'] (текст подсказки) и ['database'] (информация о базе данных, если таблица находится не в базе Друпала).

Но в рассматриваемом примере таблица не базовая, поэтому рассмотрим другой массив — $data['имя_таблицы']['table']['join']. В нем мы должны привести информацию о том, как и к какой базовой таблице Views должен присоединить данные таблицы {testnode}:

  1. //join to node
  2.   $data['testnode']['table']['join']['node'] = array(
  3.     'left_field' => 'nid',
  4.     'field'     => 'nid',
  5.   );
  6.  
  7.   return $data;
  8. } //function mytest_views_data

Поскольку в нашей таблице содержатся поля для нового типа ноды testnode, очевидно, что мы хотели бы присоединить их к таблице {node}, так чтобы поля int_count и int_total могли выводиться в Node View. Для этого в секции ['join'] мы создаем массив ['node'] (имя базовой таблицы) и указываем поля, на базе которых можно объединить данные: 'left_field' — поле в таблице {node} и 'field' — поле в таблице {testnode}. Таблицы, как видно из кода, объединяются на базе поля nid.

На этом интеграция таблицы {testnode} закончена. Модуль можно установить и Views сразу позволит добавлять к представлению поля int_count и int_total, а также сортировать и фильтровать по их значениям вывод представления.

Теперь же решим задачу добавления к «вьюхе» виртуального, то есть несуществующего в таблице {testnode} поля. Пусть оно называется percentage и вычисляется из существующих значений int_count и int_total по формуле (int_count*100/int_total), то есть представляет собой некое процентное соотношение count от total. На этом примере посмотрим, как добавить виртуальное поле во Views, как пользоваться хэндлерами с суффиксом formula и как писать свои унаследованные от существующих хэндлеры.

Во-первых, нам необходимо добавить поле percentage, для чего, как мы уже знаем, служит элемент ['field']:

  1.   //... продолжение хука mytest_views_data
  2.   $data['testnode']['percentage'] = array(
  3.     'title' => t('Percentage'),
  4.     'field' => array(
  5.       'handler' => 'mytest_handler_field_percentage',
  6.     ),
  7.   //...

Очевидно, мы не можем использовать стандартный field-хэндлер, поскольку он добавит в SQL-запрос поле 'percentage', а в таблице {testnode} такого поля нет. Вместо этого мы бы хотели ничего не добавлять в запрос, а на выходе получить результат в виде числа. Для этого нам потребуется собственный хэндлер, который мы назовем mytest_handler_field_percentage. Общепринятая практика при программировании под Views рекомендует, чтобы первым словом в названии хэндлера было имя модуля, затем слово handler и та опция Views, которой он управляет (то есть field), и в конце название поля таблицы БД. Реализацию хэндлера разместим в файле mytest_handler_field_percentage.inc:
  1. <?php
  2. /**
  3.  * Класс генерирует виртуальное поле, равное round(int_count*100/int_total);
  4.  */
  5. class mytest_handler_field_percentage extends views_handler_field_numeric {
  6.   function query() {
  7.     return ;
  8.   }
  9.   function render($values) {
  10.     if (isset($values->testnode_int_count) && isset($values->testnode_int_total)) {
  11.       return round($values->testnode_int_count*100/$values->testnode_int_total);
  12.     } else {
  13.       return 0;
  14.     }
  15.   }
  16. }

В качестве родительского мы берем хэндлер views_handler_field_numeric, так как этот класс уже умеет обращаться с численными полями, а наше виртуальное поле — это число. В первую очередь мы переопределяем метод query(), который отвечает за модификацию SQL-запроса. В нашем случае мы не хотим ничего добавлять в запрос, поэтому метод query() — пустой.

Затем озаботимся результатом, который будет записан в поле после выполнения запроса Views. За это отвечает метод render($values), где в объекте $values находятся все считанные из БД значения. В переопределенном методе render() мы вычисляем и возвращаем результат по считанным из базы полям int_count и int_total. Обратите внимание, что имя таблицы {testnode} является префиксом для обоих полей. Если же int_count или int_total не были считаны из БД при выполнении запроса, метод render() вернет 0. Правда, такой подход подразумевает, что пользователь должен вручную добавить поля int_count и int_total к представлению. Если мы захотим автоматизировать процесс и добавлять эти поля всякий раз, когда нам нужно поле percentage, то необходимо реализовать метод add_additional_fields(), либо добавить поля int_count и int_total в свойство additional_field в конструкторе хэндлера, либо добавить их к запросу непосредственно в методе query().

Теперь, когда новое виртуальное поле описано, мы бы хотели иметь возможность сортировать по нему коллекции данных. Для этого подходит уже существующий хэндлер views_handler_sort_formula. Этот хэндлер дополнительным параметром требует формулу, по вычисленному значению которой будет отсортирована коллекция. Заданная формула будет вставлена в запрос Views в части ORDER BY.

  1.   //... продолжение массива $data['testnode']['percentage']
  2.   'sort' => array(
  3.       'handler' => 'views_handler_sort_formula',
  4.       'formula' => 'ROUND(testnode.int_count*100/testnode.int_total)',
  5.     ),
  6.   //...

Еще мы хотели бы научить Views фильтровать выборку из БД по виртуального полю percentage. Для этого тоже создадим новый хэндлер:

  1.   //...
  2.     'filter' => array(
  3.       'handler' => 'mytest_handler_filter_percentage',
  4.     ),

Хэндлер mytest_handler_filter_percentage разместим в одноименном .inc-файле. Наследовать его будем от numeric-фильтра:
  1. class mytest_handler_filter_percentage extends views_handler_filter_numeric {
  2.   function query() {
  3.     $this->ensure_my_table();
  4.     $field = "ROUND(testnode.int_count*100/testnode.int_total)";
  5.  
  6.     $info = $this->operators();
  7.     if (!empty($info[$this->operator]['method'])) {
  8.       $this->{$info[$this->operator]['method']}($field);
  9.     }
  10.   }
  11. }

Здесь мы переопределяем метод query() чтобы вместо названия поля в WHERE-часть запроса была добавлена наша формула. Для этого мы сначала вызовом $this->ensure_my_table() убеждаемся в том, что в запрос будет добавлено имя таблицы, в которой находятся поля int_count и int_total, затем определяем операцию, по которому происходит фильтрация, и указываем для этой операции нашу формулу. Смысл наследования numeric-фильтра в том, что для него уже определены операции сравнения чисел (больше, больше или равно, меньше и так далее). По сути, реализация метода скопирована из реализации родителя, изменить потребовалось только значение $field.

Наконец, прежде чем Views увидит наши новые хэндлеры, необходимо добавить в файл mytest.views.inc реализацию хука hook_views_handlers:

  1. function mytest_views_handlers() {
  2.   $handlers = array();
  3.  
  4.   $handlers['handlers']['mytest_handler_field_percentage'] = array(
  5.     'parent' => 'views_handler_field_numeric',
  6.     'file' => 'mytest_handler_field_percentage.inc',
  7.   );
  8.  
  9.   $handlers['handlers']['mytest_handler_filter_percentage'] = array(
  10.     'parent' => 'views_handler_filter_numeric',
  11.     'file' => 'mytest_handler_filter_percentage.inc',
  12.   );
  13.  
  14.   return $handlers;
  15. } //function mytest_views_handlers

Хук просто возвращает массив новых хэндлеров с информацией о том, какие классы-хэндлеры они наследуют и в каких файлах их искать.

Вот и все. Мы научили Views считывать, выводить и использовать для сортировки и фильтрации результатов запроса виртуальное поле percentage. Осталось только почистить кэш Views со страницы 'admin/build/views/tools' и модуль готов к работе. Задача решена.

Прикрепленный файлРазмер
mytest.tar.gz3.18 кб
allexx (гость)

Спасибо за статью. Для меня это послужило отправной точкой. За выходные более менее вник в азы и сам тоже небольшую заметку выложил по этой теме: http://www.allexx.info/drupal_views_api_1st_example

graker
Аватар пользователя graker

Пожалуйста. К сожалению, информация по Views API очень скудная не только на русском, но и на английском. Поэтому видимо инициативные пользователи Друпала должны сами стараться писать всякое про Views, чтобы набралась критическая масса статей. Тогда общими стараниями мы приблизимся к светлому будущему :)

Vladimir (гость)

Поскольку нет инфы о апи, очень помогает разбор инклудов в /views/modules, например из /views/modules/node.views.inc я узнал как выводить поле ввиде даты.

graker
Аватар пользователя graker

Вообще, по API есть информация, в заметке есть ссылки на нее.
Правда, некоторые вещи там не до конца описаны или вовсе не описаны, да, поэтому приходится выискивать их в коде.

vic (гость)

А работали с числовыми полями, которые являются ключами от массива значений?
Например хранится в базе поле статус. Но мне при работе со views хочется видеть не сухие цифры, а их значения:
array(
'1' => 'Проект',
'2' => 'На согласовании',
'3' => 'Активный',
'4' => 'Закрыт',
'5' => 'Архив',
);

Почитал документацию по API — вобще скудно

graker
Аватар пользователя graker

Навскидку — через свой филд-хэндлер и метод render() легко делается, примерно так:

  1. class mytest_handler_field_array_key extends views_handler_field {
  2.   function render($values) {
  3.     if (isset($values->my_array_key)) {
  4.       return $my_array[$values->my_array_key];
  5.     }
  6.   }
  7. }

vic (гость)

Спасибо за помощь!
Но что то не работает. Во вьюсах вылазит ошибка
«Error: handler for question > status doesn't exist!»

А попутно у меня еще задача выросла. Мне нужно добавить к вьюсам виртуальное поле, которое представляет собой сумму связанных полей из другой таблицы. Например, есть вопрос, а к нему надо добавить виртуальное поле — количество ответов для этого вопроса.
Это нужно опять же свой хендлер и определить там как то функцию function query() ?

graker
Аватар пользователя graker

vic пишет:
Спасибо за помощь!
Но что то не работает. Во вьюсах вылазит ошибка
«Error: handler for question > status doesn't exist!"
Я навскидку написал, не знаю, может вы хэндлеры не так задали или еще чего.

Цитата:
Это нужно опять же свой хендлер и определить там как то функцию function query() ?
Ага, query() и render().

andypost (гость)

Насчет документации views — все основы изложены в самом модуле, просто нужно поставить модуль advanced_help

graker
Аватар пользователя graker

Да я как-то в курсе. Речь про особенности API, а не про работу с модулем.

andypost (гость)

Дык там и особенности API расписаны, как и для панелей.

graker
Аватар пользователя graker

Угу. Там копия вот этого: http://views-help.doc.logrus.com/. Ну или наоборот, по адресу — копия advanced_help-а по Views :) По мне, так для работы с API этого не совсем достаточно. Вот тут получше: http://views2.logrus.com/doc/html/index.html, но тоже мало и недоработано, в куче мест просто куски кода, без какого-либо описания логики.

Отправить комментарий

CAPTCHA
Пройдите, пожалуйста, проверку. Она нужна, чтобы отличать людей от спам-роботов. А если не хотите проходить эту проверку регулярно — зарегистрируйтесь.
Image CAPTCHA
Введите цифры, изображенные на картинке (без пробелов).