Drupal 7: Как я создавал один свой проект без Views

Views это прекрасный модуль для построения запросов в базу данных используемый практически на каждом Drupal сайте, в 8й версии вошедший в ядро. Архитектура этого плагина безусловно сложная, но чрезвычайно мощная. На данный момент существует 125 дополнительных модулей расширяющих функциональность Views. Так почему-же я решил отказаться от использования данного модуля?

Когда я начал разработку очередного сайта на друпал, я решил немного переосмыслить используемые мной приёмы и задал себе два вопроса:

  • Предоставляет ли Views надежный API (т. е. могу ли я использовать его без интерфейса)?
  • Действительно ли мне это нужно, только по тому, что каждый сайт на друпал сделан таким образом? Не избыточен ли его функционал?

На самом деле второй вопрос содержит в себе сразу несколько! В первом случае я бы не раздумывая ответил утвердительно, но вот со вторым вопросом всё обстоит не так очевидно. В прошлых проектах мне часто приходилось вместо разработки разбираться (боротся) с тонкостями настройки данного модуля, в то время как сделать определённый запрос куда более просто было бы вручную.

Итак, что же я решил использовать вместо Views? Конечно же EntityFieldQuery, это класс в Drupal 7 который позволяет извлекать набор объектов на основе заданных условий. EntityFieldQuery позволяет находить объекты на основе параметров объекта, значений полей и других метаданных объекта. Синтаксис данного класса компактен, выразителен и прост в использовании. И самое главное это часть ядра Drupal что освобождает нас от установки дополнительных модулей.

Простой EntityFieldQuery, запрос который выведет 5 последних нод из нескольких типов, например: статьи, товары и блоги, созданных автором текущей записи, что-то типа - "Другие материалы данного автора", может выглядеть следующим образом:

$node = menu_get_object();
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'node') ->propertyCondition('status', 1)
  ->propertyCondition('type', array('article', 'product', 'blog'))
  ->propertyCondition('uid', $node->uid)
  ->propertyOrderBy('created', 'DESC')
  ->range(0, 5);
$result = $query->execute();

Обратите внимание на некоторые детали:

  • Все методы класса EntityFieldQuery обычно связаны. То есть они возвращают измененный объект EntityFieldQuery. Вам можете быть знаком такой подход, например по аналогии с библиотекой jQuery.
  • Подобно построению запросов Drupal 7, методы EntityFieldQuery имеют операторы по умолчанию, которые они предполагают, и являются довольно гибкими с точки зрения того, какие значения они будут принимать. Так, например, propertyCondition() содержит оператор '=', либо 'IN' в качестве оператора по умолчанию, в зависимости от того, передаете ли вы ему строку, число или массив в качестве значения сравнения. Конечно, если вы хотите использовать другой оператор сравнения, например:  «<>» или «NOT IN» вы можете передать это явно.
  • Обратите внимание, что на данном этапе мы всё еще не запрашиваем поля. EntityFieldQuery начинает творить настоящую магию, в момент когда вы начинаете запрашивать значения поля, так как он сам заботится о поиске соответствующей таблицы поля и выполняет объединение данных в объект за вас.

EntityFieldQuery действительно мощный инструмент, но разумеется он не делает всю работу за нас. В данном проекте я использовал Organic Groups, и мне хотелось, чтобы мои запросы видели группы, не заставляя меня добавлять их каждый раз вручную. Кроме того, почти во всех случаях я запрашивал ноды, которые были опубликованы. К счастью, EntityFieldQuery - это обычный PHP класс, поэтому его очень легко расширить. Таким образом я создал наследование EnergyEntityFieldQuery.

class EnergyEntityFieldQuery extends EntityFieldQuery {
  /**
   * применяем некоторые значения по умолчанию ко всем экземплярам этого объекта
   */
  public function __construct() {
    $this
      // выбираем ноды
      ->entityCondition(‘entity_type’, ‘node’)
      // Со статусом опубликованно
      ->propertyCondition(‘status’, 1)
      // по умолчанию меняем хронологический порядок
      ->propertyOrderBy(‘created’, ‘DESC’);
    /* make assumption that we want group content; see method below */
    $this->setPrimaryAudienceCondition();
  }
 
  /**
   * Вспомогательная функция для запросов по терминам словаря
   * Ищет по имени термина для удобства; tid тоже распознаётся.
   *
   * @param $topics
   *    Строка, число или массив; при необходимости преобразуем в массив
   */
  public function setTopicCondition($topics) {
    $topics = !is_array($topics) ? array($topics) : $topics;
    if (count($topics)) {
      // сли термин не является числовым, выполните поиск для каждого термина и замените его на tid
      foreach ($topics as $idx => $topic) {
        // попытаемся найти tid для нечисловых терминов
        if (!is_numeric($topic)) {
          // ищем
          $vocab = taxonomy_vocabulary_machine_name_load(‘topics’);
          $candidate_terms = taxonomy_get_term_by_name($topic);
          foreach ($candidate_terms as $candidate) {
            if ($candidate->vid == $vocab->vid) {
              $topics[$idx] = $candidate->tid;
            }
          }
        }
      }
      // field_topic_term - это наше поле со ссылкой на термин таксономии
      // как только мы преобразуем все наши термины в tid, мы устанавливаем их как числовое условие для нашего поиска
      $this->fieldCondition(‘field_topic_term’, ‘tid’, $topics);
    }
    return $this;
  }
 
  /**
   * Добавляем условие для поиска по полю первичной аудитории.
   * EnergyEntityFieldQuery предполагает, что мы хотим, чтобы контент соответствовал текущей группе.
   * Класс предоставит метод отмены
   *
   * @param $gid
   *   An array or integer for the gid(s) to search the primary audience field
   *   based on. If empty, will try to pull current group from the page context.
   */
  public function setPrimaryAudienceCondition($gid = NULL) {
    if (empty($gid)) {
      $current_group = og_context_determine_context();
      $gid = $current_group->gid;
    }
 
    if (!empty($gid)) {
      $this->fieldCondition(‘group_audience’, ‘gid’, $gid);
    }
    return $this;
  }
 
  /**
   * Unset group content conditions
         *
         * Use this method if you do not want to filter by group content.
   */
  public function clearAudienceConditions() {
    foreach ($this->fieldConditions as $idx => $fieldCondition) {
      $field_name = $fieldCondition[‘field’][‘field_name’];
      if (($field_name === ‘group_audience’) || ($field_name === ‘group_audience_other’)) {
        unset($this->fieldConditions[$idx]);
      }
    }
    return $this;
  }
 
  /**
   * If we’re currently on a node, and if the entity_type is node, exclude the local node from the query.
   * This prevents the node the user is viewing from showing up in queries.
   */
  public function excludeNode($nid) {
    if (!$nid) {
      $object = menu_get_object();
      $nid = $object->nid;
    }
    if (!empty($nid) && $this->entityConditions[‘entity_type’][‘value’] === ‘node’) {
      $this->propertyCondition(‘nid’, $nid, ‘<>’);
    }
    return $this;
  }
 
}

Обратите внимание, что также можно переопределить защищенные методы самого EntityFieldQuery. Это может быть полезно, например, если у вас есть более сложные требования к параметрам, чем предоставляет EntityFieldQuery. Таким образом я обнаружил, что смог решить около 90% случаев использования вывода списков нод при помощи EnergyEntityFieldQuer.

Для одного из клиентов я создал простой пользовательский интерфейс, который позволяет передавать параметры классу, что позволяет конечному пользователю менять вывод запросов, например менять количество или категорию выводимых нод.

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

Простая часть. Помните, что использование EnergyEntityFieldQuery позволяет нам ограничивать наши запросы содержимым определённой группы OG и задавать тип нод по умолчанию.

$query = new EnergyEntityFieldQuery();

Предположим, что нам нужны только ноды, которые являются скидками.

$query->entityCondition(‘bundle’, ‘rebate’);

EntityFeildQuery имеет встроенные функцию пагинации, которую очень просто добавить:

$query->pager(10);

Вытягиваем условия из строки запроса для создания фильтров на основе таксономии:

function energy_rebate_savings_search($filters = array()) {
  // Сопоставляем машинное имя словаря с именами полей для ссылок термина на этот словарь.
  $term_field_map = array(
    ‘rebate_provider’ => ‘field_rebate_provider’,
    ‘rebate_savings_for’ => ‘field_rebate_savings_for_short’,
    ‘rebate_eligibility’ => ‘field_rebate_eligibility_short’,
  );
  // Получаем не ‘q’ параметры
  $params = drupal_get_query_parameters();
  $param_filters = array();
  foreach (array_keys($term_field_map) as $vocab) {
    if (isset($params[$vocab])) {
      $param_filters[$vocab] = $params[$vocab];
    }
  }
  $filters = array_merge($param_filters, $filters);
  // устанавливаем условия для терминов если таковые имеются
  foreach ($filters as $filter => $value) {
    if ($value != 0) {
      $query->fieldCondition($term_field_map[$filter], ‘tid’, $value);
    }
  }
  // Выполняем запрос.
  $result = $query->execute();
  // обработка и темизация не входит в часть этого примера
}

Таким образом данная функция возвращает нам массив id нод (nids), которые соответствуют условиям, нашего запроса. Получив их, мы сможем обрабатывать этот список любым способом. Мой любимый метод заключается в использовании значительно расширенной концепции режимов просмотра Drupal 7, о которой я расскажу в следующей заметке. Что бы не оставлять вас с пустыми руками, дам вам подсказку, я использую модуль Bean, это модуль для замены блоков Views. Beans можно использовать для выполнения более гибкого подхода представления информации в блоках посредством API.