При разработке модулей для 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()
:
function mytest_views_api() {
return array('api' => 2);
} //mytest_views_api
Затем создадим файл mytest.views.inc, в котором модуль Views самостоятельно сможет найти вызов нужных хуков. Пришла пора рассказать Views про нашу таблицу. Для этого используется хук hook_views_data()
, возвращающий массив с информацией об описываемых таблицах и их полях. Реализация хука должна возвращать ассоциативный массив формата $data['имя_таблицы']['имена_полей']
. Также в массиве для каждой таблицы может содержаться подмассив $data['имя_таблицы']['table']
с общей информацией о таблице. Рассмотрим хук по порядку:
function mytest_views_data() {
$data = array();
$data['testnode'] = array(
'table' => array(<br>
'group' => t('Testnode'),
),
//...
Подмассив ['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}
мы опишем так:
//... это продолжение хука mytest_views_data
'tid' => array(
'title' => t('Test Node ID'),
'field' => array(
'handler' => 'views_handler_field',
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
),
'nid' => array(
'title' => t('Node ID'),
'field' => array(
'handler' => 'views_handler_field',
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
),
'int_count' => array(
'title' => t('Count'),
'field' => array(
'handler' => 'views_handler_field',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
),
'int_total' => array(
'title' => t('Total'),
'field' => array(
'handler' => 'views_handler_field',
),
'sort' => array(
'handler' => 'views_handler_sort',
),
'filter' => array(
'handler' => 'views_handler_filter_numeric',
),
),
);
Теперь необходимо снова вернуться к секции массива ['table']
. Помимо имени группы, в ней содержится важная информация о том, базовая ли это таблица, или ее нужно присоединять к другой при генерации представления. Базовая таблица — это таблица, центральная для Views, как например таблица node для Node view или таблица user — для User view. Будь наша таблица базовой, мы задали бы для нее в массиве $data['имя_таблицы']['table']['base']
элементы ['field']
(название поля-первичного ключа), ['title']
(заголовок таблицы), ['help']
(текст подсказки) и ['database']
(информация о базе данных, если таблица находится не в базе Друпала).
Но в рассматриваемом примере таблица не базовая, поэтому рассмотрим другой массив — $data['имя_таблицы']['table']['join']
. В нем мы должны привести информацию о том, как и к какой базовой таблице Views должен присоединить данные таблицы {testnode}
:
//join to node
$data['testnode']['table']['join']['node'] = array(
'left_field' => 'nid',
'field' => 'nid',
);
return $data;<br>
} //function mytest_views_data<br><br>
Поскольку в нашей таблице содержатся поля для нового типа ноды 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 и как писать свои унаследованные от существующих хэндлеры.
Во-первых, нам необходимо добавить поле , для чего, как мы уже знаем, служит элемент ['field']
:
//... продолжение хука mytest_views_data
$data['testnode']['percentage'] = array(
'title' => t('Percentage'),
'field' => array(
'handler' => 'mytest_handler_field_percentage',
),
//...
Очевидно, мы не можем использовать стандартный field-хэндлер, поскольку он добавит в SQL-запрос поле percentage
, а в таблице {testnode}
такого поля нет. Вместо этого мы бы хотели ничего не добавлять в запрос, а на выходе получить результат в виде числа. Для этого нам потребуется собственный хэндлер, который мы назовем mytest_handler_field_percentage
. Общепринятая практика при программировании под Views рекомендует, чтобы первым словом в названии хэндлера было имя модуля, затем слово handler и та опция Views, которой он управляет (то есть field), и в конце название поля таблицы БД. Реализацию хэндлера разместим в файле mytest_handler_field_percentage.inc:
/**
* Класс генерирует виртуальное поле, равное round(int_count*100/int_total);
*/
class mytest_handler_field_percentage extends views_handler_field_numeric {
function query() {
return ;
}
function render($values) {
if (isset($values->testnode_int_count) && isset($values->testnode_int_total)) {
return round($values->testnode_int_count*100/$values->testnode_int_total);
} else {
return 0;
}
}
}
В качестве родительского мы берем хэндлер 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.
//... продолжение массива $data['testnode']['percentage']
'sort' => array(
'handler' => 'views_handler_sort_formula',
'formula' => 'ROUND(testnode.int_count*100/testnode.int_total)',
),
//...
Еще мы хотели бы научить Views фильтровать выборку из БД по виртуального полю percentage
. Для этого тоже создадим новый хэндлер:
//...
'filter' => array(
'handler' => 'mytest_handler_filter_percentage',
),
Хэндлер mytest_handler_filter_percentage
разместим в одноименном .inc-файле. Наследовать его будем от numeric-фильтра:
class mytest_handler_filter_percentage extends views_handler_filter_numeric {
function query() {
$this->ensure_my_table();
$field = "ROUND(testnode.int_count*100/testnode.int_total)";$info = $this->operators();
if (!empty($info[$this->operator]['method'])) {
$this->{$info[$this->operator]['method']}($field);
}
}
}
Здесь мы переопределяем метод query()
чтобы вместо названия поля в WHERE-часть запроса была добавлена наша формула. Для этого мы сначала вызовом $this->ensure_my_table()
убеждаемся в том, что в запрос будет добавлено имя таблицы, в которой находятся поля int_count
и int_total
, затем определяем операцию, по которому происходит фильтрация, и указываем для этой операции нашу формулу. Смысл наследования numeric-фильтра в том, что для него уже определены операции сравнения чисел (больше, больше или равно, меньше и так далее). По сути, реализация метода скопирована из реализации родителя, изменить потребовалось только значение $field
.
Наконец, прежде чем Views увидит наши новые хэндлеры, необходимо добавить в файл mytest.views.inc реализацию хука hook_views_handlers
:
function mytest_views_handlers() {
$handlers = array();
$handlers['handlers']['mytest_handler_field_percentage'] = array(
'parent' => 'views_handler_field_numeric',
'file' => 'mytest_handler_field_percentage.inc',
);
$handlers['handlers']['mytest_handler_filter_percentage'] = array(
'parent' => 'views_handler_filter_numeric',
'file' => 'mytest_handler_filter_percentage.inc',
);
return $handlers;
} //function mytest_views_handlers
Хук просто возвращает массив новых хэндлеров с информацией о том, какие классы-хэндлеры они наследуют и в каких файлах их искать.
Вот и все. Мы научили Views считывать, выводить и использовать для сортировки и фильтрации результатов запроса виртуальное поле percentage
. Осталось только почистить кэш Views со страницы 'admin/build/views/tools'
и модуль готов к работе. Задача решена.