---
title: 'Как подружить свой тип данных с Views'
url: 'https://graker.ru/news/2010/02/24/views_integration'
language: ru
type: post
author: 'Роман Грачев'
published: '2010-02-24T00:25:29+03:00'
updated: '2017-03-13T13:09:45+03:00'
categories:
  - Программирование
  - Друпальчики
---

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

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

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

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

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

Предполагаю, что читатель уже умеет программно создавать свои типы материалов. Рассказывать буду на примере модуля, в котором новый тип уже создан. А чтобы не публиковать код модуля с описанием нового типа, предлагаю просто его скачать:
[Скачать модуль mytest](https://graker.ru/sites/default/files/legacy/mytest.tar.gz).
В модуле уже реализовано все, о чем дальше пойдет речь в заметке.

Итак, у нас есть тип материала `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](http://views2.logrus.com/doc/html/index.html "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}` мы опишем так:

```php
    //... это продолжение хука 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'` и модуль готов к работе. Задача решена.
