Создание html таблицы с сортировкой и пагинацией в Drupal 7

Пример вывода списка нод по 10 штук на страницу, с возможностью сортировки по заголовку, дате и типу ноды:

$header = array(
  array('data' => 'Заголовок',     'field' => 'title'),
  array('data' => 'Дата создания', 'field' => 'created'),
  array('data' => 'Тип',           'field' => 'type'),
);
 
$nodes = db_select('node', 'n')
  ->fields('n', array('title', 'created', 'type'))
  ->extend('PagerDefault')
  ->limit(10)
  ->extend('TableSort')
  ->orderByHeader($header)
  ->execute();
 
$rows = array();
foreach ($nodes as $node) {
  $rows[] = array(
    check_plain($node->title),
    format_date($node->created),
    $node->type
  );
}
 
$output = theme('table', array('header' => $header, 'rows' => $rows));
$output .= theme('pager');


Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:33:23 Добавил: Андрей Ковальчук

Группируем поля в вертикальные вкладки (Vertical Tabs)

В Drupal 7 появился новый вид группировки полей — Vertical Tabs:

Чтобы воспользоваться такими вкладками в свой форме, нужно:

1. Сгруппировать поля в обычные fieldset-ы:

$form['fieldset1'] = array(
  '#type' => 'fieldset',
  '#title' => 'Fieldset 1',
);
 
$form['fieldset1']['field1'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 1',
);
 
$form['fieldset1']['field2'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 2',
);
 
$form['fieldset2'] = array(
  '#type' => 'fieldset',
  '#title' => 'Fieldset 2',
);
 
$form['fieldset2']['field3'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 3',
);
 
...

2. Добавить к форме новый элемент с типом vertical_tabs:
$form['vertical_tabs'] = array(
  '#type' => 'vertical_tabs',
);

3. Добавить fieldset-ам новый параметр '#group', в котором прописать имя элемента из пункта 2:
$form['fieldset1'] = array(
  ...
  '#group' => 'vertical_tabs',
);
 
$form['fieldset2'] = array(
  ...
  '#group' => 'vertical_tabs',
);

Profit.

Полный листинг:
$form['vertical_tabs'] = array(
  '#type' => 'vertical_tabs',
);
 
$form['fieldset1'] = array(
  '#type' => 'fieldset',
  '#title' => 'Fieldset 1',
  '#group' => 'vertical_tabs',
);
 
$form['fieldset1']['field1'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 1',
);
 
$form['fieldset1']['field2'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 2',
);
 
$form['fieldset2'] = array(
  '#type' => 'fieldset',
  '#title' => 'Fieldset 2',
  '#group' => 'vertical_tabs',
);
 
$form['fieldset2']['field3'] = array(
  '#type' => 'textfield',
  '#title' => 'Field 3',
);
 
...

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:31:00 Добавил: Андрей Ковальчук

Произвольное имя директории домена при мультисайтинге

При мультисайтинге, имя директории в папке sites должно совпадать с именем домена. Например если адрес у сайта mysite.ru, то его личные файлы должны хранится в sites/mysite.ru.

Однако в Drupal 7 существует возможность дать директории произвольное имя. Для этого нужно скопировать файл sites/example.sites.php в sites/sites.php и прописать в нём имя в формате:

$sites['domenname'] = 'dirname';

Например:

$sites['mysite.ru'] = 'mysite';

Таким же образом можно сделать так, чтобы несколько доменов (например dev и production) имели одну директорию:

$sites['mysite.ru'] = 'mysite';
$sites['mysite.local'] = 'mysite';

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:26:44 Добавил: Андрей Ковальчук

Модуль Simpleping — уведомление поисковиков о новом контенте

Модуль Simpleping — это крохотный аналог Drupal 6 модуля Multiping. Модуль уведомляет поисковики о новом/обновлённом контенте.

При каждом создании или обновлении ноды, посылается ping на два адреса:

http://ping.blogs.yandex.ru/RPC2
http://rpc.pingomatic.com

Первый пинг уведомляет яндекс, второй — все остальные популярные поисковики.

Установка стандартная — распаковать в sites/all/modules, включить.

Написанное актуально для

Drupal 7

Добавлено: 29 Июля 2018 22:25:37 Добавил: Андрей Ковальчук

Сниппет вывода последних материалов

Сниппет выводит ссылки на 5 последних добавленных материалов типа article:

<?php
$nodes = db_select('node', 'n')
  ->fields('n', array('nid', 'title'))
  ->condition('n.type', 'article')
  ->condition('n.status', NODE_PUBLISHED)
  ->orderBy('n.created', 'DESC')
  ->range(0, 5)
  ->execute();
$titles = node_title_list($nodes);
echo drupal_render($titles);
?>

Что такое сниппет и как им воспользоваться.

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:21:26 Добавил: Андрей Ковальчук

Как создать свой токен для поля материала

Как известно, из-за спешки зарелизить Drupal 7, в ядро не включили токены для полей (field tokens, [node:field-name]). Отсутствующий функционал пытались добавить майнтейнеры оригинального модуля Token, но из-за разногласий пока ничего не вышло.

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

Пример модуля, который создаёт один единственный токен для текстового поля field_category:

/**
 * Implements hook_token_info().
 */
function mymodule_token_info() {
  $tokens['category'] = array(
    'name' => t('Category'),
    'description' => t('The category of the node.'),
  );
  return array(
    'tokens' => array('node' => $tokens),
  );
}
 
/**
 * Implements hook_tokens().
 */
function mymodule_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
 
  if ($type == 'node' && !empty($data['node'])) {
    $node = $data['node'];
 
    foreach ($tokens as $name => $original) {
      if ($name == 'category') {
        $replacements[$original] = $node->field_category['und'][0]['value'];
      }
    }    
  }
 
  return $replacements;
}

Написанное актуально для
Drupal 7.x

Добавлено: 29 Июля 2018 22:20:39 Добавил: Андрей Ковальчук

Подзапросы в Database API семёрки

Пример использование подзапроса в SELECT:

$subquery = db_select('node', 'n');
$subquery->addExpression('COUNT(*)');
$subquery->where('n.uid = u.uid');
 
$query = db_select('users', 'u');
$query->fields('u', array('uid', 'name'));
$query->addExpression('(' . $subquery . ')', 'nodes');
$users = $query->execute();

Код выбирает пользователей и подсчитывает созданные ими ноды. Он равносилен запросу:

$users = db_query("
  SELECT u.uid, u.name, (
    SELECT COUNT(*)
    FROM {node} n
    WHERE n.uid = u.uid
  ) AS nodes
  FROM {users} u
");

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

$subquery->condition('поле', 'аргумент'); // fail
$subquery->addExpression('выражение', 'алиас', 'аргументы'); // fail

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:19:23 Добавил: Андрей Ковальчук

Выборка постов с определёнными тегами

Есть таблицы posts (id, title, text) и tags (id, name), связанные отношением многие-ко-многим с помощью таблицы posts_tags (post_id, tag_id).

Задача — выбрать посты с определёнными тегами.

Вариант 1:

SELECT p.id, p.title, p.text
FROM posts p
INNER JOIN posts_tags pt1 ON pt1.post_id = p.id
INNER JOIN posts_tags pt2 ON pt2.post_id = p.id
WHERE
    pt1.tag_id = ID_ПЕРВОГО_ТЕГА AND
    pt2.tag_id = ID_ВТОРОГО_ТЕГА

Таблица posts_tags джойнится столько раз, сколько тегов участвует в выборке (в примере их два).

Вариант 2:

SELECT p.id, p.title, p.text
FROM posts p
WHERE p.id IN (
    SELECT pt.post_id
    FROM posts_tags pt
    INNER JOIN posts_tags pt1 ON pt.post_id = pt1.post_id AND pt1.tag_id = ID_ПЕРВОГО_ТЕГА
    INNER JOIN posts_tags pt2 ON pt.post_id = pt2.post_id AND pt2.tag_id = ID_ВТОРОГО_ТЕГА
)

Таким способом делает выборку модуль Views в Drupal.

Вариант 3:

SELECT p.id, p.title, p.text
FROM posts p
INNER JOIN posts_tags pt ON pt.post_id = p.id
WHERE pt.tag_id IN (ID_ПЕРВОГО_ТЕГА, ID_ВТОРОГО_ТЕГА)
GROUP BY p.id
HAVING COUNT(*) >= 2

В HAVING указывается количество тегов участвующих в выборке (в примере их два). Таким способом делает выборку модуль Search в Drupal.

Добавлено: 29 Июля 2018 22:18:40 Добавил: Андрей Ковальчук

Как удалить shortlink из &lt;head&gt; страничек

Drupal 7 на каждой странице материала выводит shortlink, даже если у ноды есть синоним:

<link rel="shortlink" href="/node/..." />

Избавиться от него можно так:

function THEMENAME_html_head_alter(&$head_elements) {
  foreach ($head_elements as $key => $element) {
    if (isset($element['#attributes']['rel']) && $element['#attributes']['rel'] == 'shortlink') {
      unset($head_elements[$key]);
    }
  }
}

Код добавляется в template.php. По аналогии можно удалить любой другой элемент из head.

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:17:46 Добавил: Андрей Ковальчук

Как изменить html код поля после его темизации

Пример изменения html кода поля field_price:

function THEMENAME_preprocess_node(&$vars) {
  $vars['content']['field_price']['#post_render'][] = 'THEMENAME_field_price_post_render_callback';
}
 
function THEMENAME_field_price_post_render_callback($children, $elements) {
  return '<div class="new-field-wrapper">' . $children . '</div>';
}

Но предпочтительней конечно пользоваться темизацией полей.

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:16:43 Добавил: Андрей Ковальчук

Как назначить один шаблон для нескольких полей

Например нужно поля field_price и field_count выводить с помощью шаблона field--clean.tpl.php:

/**
 * Preprocess function for theme_field().
 */
function THEMENAME_preprocess_field(&$variables) {
  if (in_array($variables['element']['#field_name'], array('field_price', 'field_count'))) {
    $variables['theme_hook_suggestions'][] = 'field__clean';
  }
}

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:16:02 Добавил: Андрей Ковальчук

Кэширование в Render API

В Drupal 7, в калбаках меню или любых других функциях, которые что-то выводят на экран, принято возвращать "рендер массив". Преимуществом такого подхода является возможность альтерить данные перед их преобразованием в html.

В Render API есть система кэширования, которая включается при добавлении в рендер массив параметра #cache. Пример ниже.

Как это было в Drupal 6:

/**
 * Menu callback
 */
function mymodule_page() {
  if ($cache = cache_get('mymodule:titles')) {
    $output = $cache->data;
  }
  else {
    $output = theme('item_list', mymodule_get_titles());
    cache_set('mymodule:titles', $output, 'cache', time() + 60*60);
  }
  return $output;
}

Как нужно делать в Drupal 7:

/**
 * Menu callback
 */
function mymodule_page() {
  $build['titles'] = array(
    '#theme' => 'item_list',
    '#items' => array(),
    '#pre_render' => array('mymodule_page_titles_pre_render_callback'),
    '#cache' => array(
      'keys' => array('mymodule', 'titles'),
      'bin' => 'cache',
      'expire' => REQUEST_TIME + 60*60,
    ),
  );
  return $build;
}
 
/**
 * Pre render callback
 */
function mymodule_page_titles_pre_render_callback($element) {
  $element['#items'] = mymodule_get_titles();
  return $element;
}

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:14:14 Добавил: Андрей Ковальчук

Как подключить внешний js или css файл

Drupal 7:

drupal_add_js('http://example.com/scripts.js', array('type' => 'external'));
drupal_add_css('http://example.com/style.css', array('type' => 'external'));
 
// В рендер-массиве
$form['#attached']['js'][] = array('data' => 'http://example.com/scripts.js', 'type' => 'external');
$form['#attached']['css'][] = array('data' => 'http://example.com/style.css', 'type' => 'external');

Drupal 6:

drupal_set_html_head('<script type="text/javascript" src="http://example.com/scripts.js"></script>');
drupal_set_html_head('<link type="text/css" rel="stylesheet" href="http://example.com/style.css" />');

Написанное актуально для
Drupal 7, Drupal 6

Добавлено: 29 Июля 2018 22:12:08 Добавил: Андрей Ковальчук

Запрещаем удалять закэшированные страницы при автоматическом запуске крона

В друпале есть два типа кэширования страниц:

1. страницы кэшируются на определённое время
2. страницы кэшируются до первой очистки кэша

На большинстве не очень посещаемых сайтах есть смысл использовать только второй тип кэширования (про boost пока не говорим). Т.е. анонимные посетители будут видеть всегда актуальную информацию взятую из кэша. Кэш же будет сбрасываться при первой активности авторизованного пользователя (например при создании/обновлении ноды) или при очистке.

Всё бы хорошо, но единственная неприятность, что при запуске крона вызывается та самая очистка кэша. И если крон запускается раз в день, то страницы будут храниться в кэше максимум день, что в общем то не совсем рационально.

Вот такой хитрый ход позволяет запретить удаление закэшированных страниц при автоматическом запуске крона:

/**
 * Implements of hook_cron().
 */
function mymodule_cron() {
  if (strpos(request_uri(), 'cron.php')) {
    if (!db_table_exists('cache_page_temp')) {
      $schema = drupal_get_schema('cache_page');
      db_rename_table('cache_page', 'cache_page_temp');
      db_create_table('cache_page', $schema);
    }
    drupal_register_shutdown_function('mymodule_restore_cache_page');
  }
}
 
/**
 * Shutdown function for cron cleanup.
 */
function mymodule_restore_cache_page() {
  db_drop_table('cache_page');
  db_rename_table('cache_page_temp', 'cache_page');
}

Плюс нужно отключить запуск крона средствами друпала (admin/config/system/cron) и возложить эту обязанность на кронтаб.

Теперь если на сайте не было никаких изменений, то страницы могут жить в кэше вечно :)

Написанное актуально для
Drupal 7

Добавлено: 29 Июля 2018 22:10:13 Добавил: Андрей Ковальчук