Pull to refresh

Drupal 7 и Fields API

Иногда, по тем или иным причинам, нам придётся создавать свои собственные типы полей. Причин может быть много:
  1. Мы хотим концептуально рассматривать какую-либо информацию, как нечто атомарное, а не составленное из более мелких частей.
  2. У нас есть комплексная информация, и мы хотим иметь много её значений в каждой сущности.
  3. Мы хотим предоставлять пользователю удобный интерфейс для редактирования нашего типа.
  4. Мы хотим отображать пользователю наш тип, в собственном формате.

Для всех этих целей в Друпале предусмотрен удобный инструмент, называемый Fields API. В данном уроке мы создадим своё поле «размер», где будем хранить длину, ширину и высоту наших сущностей.
P.S. Предпологается наличие некоторых базовых знаний CMF/CMS Drupal.

Как работает Fields API?


Существует несколько основных моментов в создании поля:
  • Field type(тип поля): определяет имя поля и внутреннюю структуру.
  • Field(поле): конкретная конфигурация типа поля.
  • Field instance(экземпляр поля): связь конкретного поля с узлом или подклассом сущности.
  • Widget(виджет): элемент формы, представляющий поле в редакторе контента. Этот элемент может использовать как простое текстовое поле, так и интерактивный флэш инструмент.
  • Formatter(форматтер): часть отвечающая за форматирование данных поля для отображения пользователю.

Мы создадим модуль dimfield, который будет создавать нам тип поля dimensions. Использование глубины будет опциональным.

Создаём новый тип поля.


dimfield.info

name = Dimensions field
description = A Field offering height, width, and depth
package = Drupal 7 Development
core = 7.x
files[] = dimfield.module


dimfield.install

Наиболее важной функцией, при создании типа поля, является фунция field_schema. Как можно понять из названия, она возвращает массив с информацией о Drupal Schema.
function dimfield_field_schema($field) {
if ($field['type'] == 'dimensions') {
$schema['columns']['height'] = array(
'type' => 'int',
'not null' => FALSE,
);
$schema['columns']['width'] = array(
'type' => 'int',
'not null' => FALSE,
);

$schema['indexes'] = array(
'height' => array('height'),
'width' => array('width'),
);

if ($field['settings']['num_dimensions'] == 3) {
$schema['columns']['depth'] = array(
'type' => 'int',
'not null' => FALSE,
);
$schema['indexes']['depth'] = array('depth');
}

$schema['columns']['units'] = array(
'type' => 'varchar',
'length' => 10,
'not null' => FALSE,
);

return $schema;
}
}


dimfield.module

Объявление поля. Теперь нам необходимо перегрузить хук hook_field_info(), которые возвращает массив с информацией о нашем поле.
function dimfield_field_info() {
return array(
'dimensions' => array(
'label' => t('Dimensions'),
'description' => t('This field stores a height and width, and possibly depth.'),
'settings' => array('num_dimensions' => 2),
'instance_settings' => array(
'max_height' => 0,
'max_width' => 0,
'max_depth' => 0,
),
'default_widget' => 'dimfield_combined',
'default_formatter' => 'dimfield_default',
),
);
}


Определение пустого значения. Далее необходимо описать случай с пустым значением нашего поля. Мы должны пояснить Друпалу, что мы считаем пустым значением. Когда указана длина, и не указана ширина? Или оба значения пусты?
function dimfield_field_is_empty($item, $field) {
if ($field['type'] == 'dimensions') {
if (empty($item['height']) && empty($item['width']) && ($field['settings']['num_dimensions'] == 2 || empty($item['depth']))) {
return TRUE;
}
}
return FALSE;
}


Настройка поля. Большинство полей может быть настроено из веб-интерфейса Друпала. Мы тоже предоставим эту возможность.
function dimfield_field_settings_form($field, $instance, $has_data) {
if ($field['type'] == 'dimensions') {
$settings = $field['settings'];
$form['num_dimensions'] = array(
'#type' => 'select',
'#title' => t('How many dimensions'),
'#options' => array(
2 => t('Height and width'),
3 => t('Height, width, and depth'),
),
'#default_value' => $settings['num_dimensions'],
'#required' => FALSE,
'#description' => t('Is this for a 2-dimensional or 3-dimensional object?'),
);
return $form;
}
}

function dimfield_field_instance_settings_form($field, $instance) {
$settings = $instance['settings'];

if ($field['type'] == 'dimensions') {
$form['max_height'] = array(
'#type' => 'textfield',
'#title' => t('Max height'),
'#size' => 10,
'#default_value' => $settings['max_height'],
'#description' => t('The largest allowed value for the height. Use 0 for no limit.'),
);
$form['max_width'] = array(
'#type' => 'textfield',
'#title' => t('Max width'),
'#size' => 10,
'#default_value' => $settings['max_width'],
'#description' => t('The largest allowed value for the width. Use 0 for no limit.'),
);

if ($field['settings']['num_dimensions'] == 3) {
$form['max_depth'] = array(
'#type' => 'textfield',
'#title' => t('Max depth'),
'#size' => 10,
'#default_value' => $settings['max_depth'],
'#description' => t('The largest allowed value for the depth. Use 0 for no limit.'),
);
}
}

return $form;
}


Валидация поля.
function dimfield_field_validate($obj_type, $object, $field, $instance, $langcode, &$items, &$errors) {
if ($field['type'] == 'dimensions') {
$columns = array(
'height' => 'max_height',
'width' => 'max_width',
);
if ($field['settings']['num_dimensions'] == 3) {
$columns['depth'] = 'max_depth';
}
foreach ($items as $delta => $item) {
foreach ($columns as $column => $max_key) {
if ($instance['settings'][$max_key] && !empty($item[$column]) && $item[$column] > $instance['settings'][$max_key]) {
$errors[$field['field_name']][$delta][] = array(
'error' => 'dimfield_' . $max_key,
'message' => t('%name: The %column may not be larger than %max.', array(
'%column' => $column,
'%name' => $instance['label'],
'%max' => $instance['settings'][$max_key],
)),
);
}
}
}
}
}


Создание виджета. Мы создадим два вида виджета для нашего поля: простой и посложнее. Простой будет состоять из обычных трех текстовых полей и селектора для выбора метрики(сантиметры, дюймы и т.п.). Сложный будет состоять из одного текстового поля. Также нам понадобиться маленький css файл, который мы назовём dimfield-admin.css и положим в папку с модулем.
///////////////// Widgets ///////////////////////
/**
* Implements hook_widget_info().
*/
function dimfield_field_widget_info() {
return array(
'dimfield_simple' => array(
'label' => t('Separate text fields'),
'description' => t('Allow the user to enter each dimension separately.'),
'field types' => array('dimensions'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
'dimfield_combined' => array(
'label' => t('Combined text field'),
'description' => t('Allow the user to enter all dimensions together.'),
'field types' => array('dimensions'),
'settings' => array('size' => 10),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
),
);
}

/**
* Implements hook_field_widget_settings_form().
*/
function dimfield_field_widget_settings_form($field, $instance) {
$form = array();

$widget = $instance['widget'];
$settings = $widget['settings'];

if ($widget['type'] == 'dimfield_combined') {
$form['size'] = array(
'#type' => 'textfield',
'#title' => t('Size of textfield'),
'#default_value' => $settings['size'],
'#required' => TRUE,
'#element_validate' => array('_element_validate_integer_positive'),
);
}

return $form;
}

/**
* Implements hook_field_widget_form().
*/
function dimfield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$base = $element;

if ($instance['widget']['type'] == 'dimfield_simple') {
$element['height'] = array(
'#type' => 'textfield',
'#title' => t('Height'),
'#default_value' => isset($items[$delta]['height']) ? $items[$delta]['height'] : NULL,
) + $base;
$element['width'] = array(
'#type' => 'textfield',
'#title' => t('Width'),
'#default_value' => isset($items[$delta]['width']) ? $items[$delta]['width'] : NULL,
) + $base;

if ($field['settings']['num_dimensions'] == 3) {
$element['depth'] = array(
'#type' => 'textfield',
'#title' => t('Depth'),
'#default_value' => isset($items[$delta]['depth']) ? $items[$delta]['depth'] : NULL,
) + $base;
}

$element['units'] = array(
'#type' => 'select',
'#title' => t('Units'),
'#default_value' => isset($items[$delta]['units']) ? $items[$delta]['units'] : NULL,
'#options' => dimfield_units(),
) + $base;
}
elseif ($instance['widget']['type'] == 'dimfield_combined') {
$element['#element_validate'] = array('_dimfield_combined_validate');

$default = NULL;
if (isset($items[$delta])) {
$item = $items[$delta];
if (isset($item['height'], $item['width'])) {
$default = $item['height'] . 'x' . $item['width'];
if ($field['settings']['num_dimensions'] == 3) {
$default .= 'x' . $item['depth'];
}
}
}

$element['dimfield_combined_wrapper']['#theme'] = 'dimfield_combined_wrapper';
$element['dimfield_combined_wrapper']['#attached']['css'][] = drupal_get_path('module', 'dimfield') . '/dimfield-admin.css';

$element['dimfield_combined_wrapper']['height_width_depth'] = array(
'#title' => t('Dimensions (separated by x)'),
'#type' => 'textfield',
'#default_value' => $default,
'#size' => $instance['widget']['settings']['size'],
) + $base;

$element['dimfield_combined_wrapper']['units'] = array(
'#type' => 'select',
'#title' => t('Units'),
'#default_value' => isset($items[$delta]['units']) ? $items[$delta]['units'] : NULL,
'#options' => dimfield_units(),
) + $base;
}

return $element;
}

/**
* Implements hook_theme().
*/
function dimfield_theme() {
return array(
'dimfield_combined_wrapper' => array(
'render element' => 'element',
),
);
}

/**
* Theme function for the combined_wrapper widget.
* @ingroup themeable
*/
function theme_dimfield_combined_wrapper($variables) {
$element = $variables['element'];

$hwd = drupal_render($element['height_width_depth']);
$units = drupal_render($element['units']);

return <<<END
{$hwd}
{$units}

END;
}

function _dimfield_combined_validate($element, &$form_state) {
// This function is also called when submitting the field configuration form.
// If so, skip validation as it won't work anyway.
if ($form_state['complete form']['#form_id'] == 'field_ui_field_edit_form') {
return;
}

$values = $form_state['values'];
$language = $element['#language'];
$field_name = $element['#field_name'];
$item = $values[$field_name][$language][$element['#delta']]['dimfield_combined_wrapper'];

$num_dimensions = 2;
if (array_search('depth', $element['#columns'])) {
$num_dimensions = 3;
}

// If there was nothing entered, we assume it to be an empty record and ignore it.
if (trim($item['height_width_depth'])) {
if (substr_count($item['height_width_depth'], 'x') == $num_dimensions - 1) {
if ($num_dimensions == 2) {
list($height, $width) = explode('x', $item['height_width_depth']);
$new_values = array(
'height' => trim($height),
'width' => trim($width),
'units' => $item['units'],
);
}
elseif ($num_dimensions == 3) {
list($height, $width, $depth) = explode('x', $item['height_width_depth']);
$new_values = array(
'height' => trim($height),
'width' => trim($width),
'depth' => trim($depth),
'units' => $item['units'],
);
}
form_set_value($element, $new_values, $form_state);
}
else {
form_set_error($field_name, t('You must specify all dimensions, separated by an \'x\'.'));
}
}
}

function dimfield_units($unit = NULL) {
static $units;

if (empty($units)) {
$units = array(
'inches' => t('Inches'),
'feet' => t('Feet'),
'meters' => t('Meters'),
);
}

if ($unit) {
return isset($units[$unit]) ? $units[$unit] : '';
}

return $units;
}

Файл: dimfield-admin.css
.dimfield-combined {
float: left;
margin: 0 30px 0 0;
}


Форматтер. Настал момент определиться с тем, как мы будем отдавать пользовательским глазам наше поле. Друпал позволяет контролировать обычный вывод на экран, для rss лент, для печати и т.д. с помощью форматтеров. Мы создадим их так же два вида: для вывода одного значения и для комплексного вывода.
///////////////// Formatters ///////////////////////

/**
* Implements hook_field_formatter_info().
*/
function dimfield_field_formatter_info() {
return array(
'dimfield_default' => array(
'label' => t('Default'),
'field types' => array('dimensions'),
),
'dimfield_table' => array(
'label' => t('Show as table'),
'field types' => array('dimensions'),
'settings' => array('units_as' => 'column'),
),
);
}

/**
* Implements hook_field_formatter_settings_summary().
*/
function dimfield_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];

$summary = '';

if ($display['type'] == 'dimfield_table') {
if ($settings['units_as'] == 'column') {
$summary = t('Show units as their own column');
}
else if ($settings['units_as'] == 'cell') {
$summary = t('Show units in each cell');
}
else if ($settings['units_as'] == 'none') {
$summary = t('Do not show units');
}
}

return $summary;
}

/**
* Implements hook_field_formatter_settings_form().
*/
function dimfield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];

$form = array();

if ($display['type'] == 'dimfield_table') {
$form['units_as'] = array(
'#title' => t('Show units'),
'#type' => 'select',
'#options' => array(
'column' => t('As their own column'),
'cell' => t('In each cell'),
'none' => t('Do not show units'),
),
'#default_value' => $settings['units_as'],
'#required' => TRUE,
);
}

return $form;
}

/**
* Implements hook_field_formatter_view().
*/
function dimfield_field_formatter_view($obj_type, $object, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];

switch ($display['type']) {
case 'dimfield_default':
foreach ($items as $delta => $item) {
if ($field['settings']['num_dimensions'] == 2) {
$output = t('@height @unit by @width @unit', array(
'@height' => $item['height'],
'@width' => $item['width'],
'@unit' => dimfield_units($item['units']),
));
}
elseif ($field['settings']['num_dimensions'] == 3) {
$output = t('@height @unit by @width @unit by @depth @unit', array(
'@height' => $item['height'],
'@width' => $item['width'],
'@depth' => $item['depth'],
'@unit' => dimfield_units($item['units']),
));
}
$element[$delta] = array('#markup' => $output);
}
break;
case 'dimfield_table':
$rows = array();
foreach ($items as $delta => $item) {
$row = array();
if ($settings['units_as'] == 'cell') {
$row[] = t('@value (%units)', array(
'@value' => $item['height'],
'%units' => dimfield_units($item['units']),
));
$row[] = t('@value (%units)', array(
'@value' => $item['width'],
'%units' => dimfield_units($item['units']),
));
}
else {
$row[] = $item['height'];
$ $row[] = t('@value (%units)', array(
'@value' => $item['depth'],
'%units' => dimfield_units($item['units']),
));
}
else {
$row[] = $item['depth'];
}
}
if ($settings['units_as'] == 'column') {
$row[] = dimfield_units($item['units']);
}
$rows[] = $row;
}

$header = array(t('Height'), t('Width'));
if ($field['settings']['num_dimensions'] == 3) {
$header[] = t('Depth');
}
if ($settings['units_as'] == 'column') {
$header[] = t('Units');
}

$element = array(
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
);
break;
}

return $element;
}row[] = $item['width'];
}
if ($field['settings']['num_dimensions'] == 3) {
if ($settings['units_as'] == 'cell') {
$row[] = t('@value (%units)', array(
'@value' => $item['depth'],
'%units' => dimfield_units($item['units']),
));
}
else {
$row[] = $item['depth'];
}
}
if ($settings['units_as'] == 'column') {
$row[] = dimfield_units($item['units']);
}
$rows[] = $row;
}

$header = array(t('Height'), t('Width'));
if ($field['settings']['num_dimensions'] == 3) {
$header[] = t('Depth');
}
if ($settings['units_as'] == 'column') {
$header[] = t('Units');
}

$element = array(
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
);
break;
}

return $element;
}


Используемая литература:

Книга «Drupal 7 Module Development» (Puckt Publishing, december 2010).
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.