Ошибка при создании вариантов товара и способ ее решения при использовании модуля Inline Entity Form

Совсем недавно столкнулся с ошибкой при использовании модулей Drupal Commerce и Inline Entity Form. Суть ошибки заключается в том, что у товара имеется 2 атрибута: цвет и размер. В качестве атрибутов используется два словаря "цвета" и "размеры" соответственно. При создании товаров в обоих полях используются виджеты Autocomplete term widget.

Вот тут и начинаются все проблемы, при создании нового товара, в момент добавления вариантов если атрибуты добавляются первый раз (их еще нет в словаре), они (атрибуты) начинают дублироваться и логика работы ломается:

Добавление вариантов с помощью Inline Entity Form
Добавление вариантов с помощью Inline Entity Form

Выделенных красным цветом атрибутов в словаре нет и вот, что получилось при просмотре карточки товара:

Карточка товара в Drupal Commerce
Карточка товара в Drupal Commerce

Я нашел два варианта решения данной проблемы:

  1. Не использовать виджет Autocomplete term widget;
  2. Искать и решать проблему.

Мне первый способ не подошел, потому что клиенту нужен был именно автокомплит (оно и понятно, найти нужный атрибут среди сотни не так уж и просто). Я начал искать причину и выяснил, что проблема логична и не является проблемой модулей Drupal Commerce и Inline Entity Form. Так что же происходит и почему возникает баг? А происходит следующее:

  • Когда пользователь пишет название атрибута (в нашем случае название термина), такое название ищется в словаре. В случае если похожие названия были найдены, пользователю предоставляются варианты для выбора;
  • Если пользователь выбирает существующий термин, кликнув по одному из названий или пишет точно такое же имя, то термину присваивается id существующего термина и в момент сохранения сущности термин в словарь не добавляется;
  • Если термин в словаре не найден, то термину в качестве tid временно присваивается 'autocreate' ($term->tid = 'autocreate') и в момент сохранения сущности термин сохраняется в словарь как новый.

Как раз в последнем случае и возникает баг, мы добавляем первый раз несуществующий атрибут (термин), ему в качестве tid присваивается $term->tid = 'autocreate', затем второму варианту добавляем этот же атрибут (термин), но так как атрибута еще нет в словаре, ему так же в качестве tid присваивается $term->tid = 'autocreate' и т.д., а в момент сохранения сущности ядро смотрит tid атрибутов и так как они равны 'autocreate' создает новые дублирующиеся атрибуты (термины).

Для устранения проблемы я написал маленький модуль (данный способ работает только в Drupal 7.8 и выше). Первым делом имплементируем хук hook_field_widget_form_alter:

/**
 * Implements hook_field_widget_form_alter().
 */
function moduleName_field_widget_form_alter(&$element, &$form_state, $context) {

  // Здесь мы добавляем дополнительный валидатор ко всем полям таксономии у которых в качестве виджета используется
  // "Autocomplete term widget".
  if ($context['field']['type'] == 'taxonomy_term_reference' && $context['instance']['widget']['type'] == 'taxonomy_autocomplete') {
    $element['#element_validate'][] = '_moduleName_taxonomy_autocomplete_validate';
  }
}

Далее добавляем свой валидатор:

/**
 * Custom taxonomy autocomplete validation callback.
 */
function _moduleName_taxonomy_autocomplete_validate($element, &$form_state) {
  $value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);

  foreach ($value as &$item) {
    
    // Если у термина в качестве tid установлено значение 'autocreate', то добавляем к термину флаг,
    // который будем использовать для дополнительной валидации в момент сохранения.
    if ($item['tid'] == 'autocreate') {
      $item['need_validate'] = TRUE;
    }
  }

  form_set_value($element, $value, $form_state);
}

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

/**
 * Implements hook_field_attach_presave().
 */
function moduleName_field_attach_presave($entity_type, $entity) {

  // Если тип сохраняемой сущности термин и у него имеется флаг, который мы установили
  // в предыдущей функции, то делаем дополнительную проверку.
  if ($entity_type == 'taxonomy_term' && !empty($entity->need_validate)) {
    unset($entity->need_validate);

    // Проверяем в базе наличие термина с таким же именем.
    $term = db_select('taxonomy_term_data', 't')
      ->fields('t', array('tid', 'name'))
      ->condition('t.vid', $entity->vid)
      ->condition('t.name', drupal_strtolower(trim($entity->name)))
      ->execute()
      ->fetchAssoc();

    // Если термин с таким именем уже существует, то присваиваем сохраняемому термину
    // имя и id существующего термина.
    if (!empty($term)) {
      $entity->name = $term['name'];
      $entity->tid = $term['tid'];
    }
  }
}

Комментарии (5)

Аватар пользователя xandeadx
xandeadx

> Проверяем в базе наличие термина с таким же именем.

почему не taxonomy_term_load()?

Аватар пользователя xandeadx
xandeadx

туплю — taxonomy_get_term_by_name()

Аватар пользователя Benya
Benya

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

Аватар пользователя xandeadx
xandeadx

экономия на спичках

timer_start('foo');
$term = taxonomy_get_term_by_name('example');
echo timer_read('foo') . ' ms';

~4 ms

Аватар пользователя Benya
Benya

Буду иметь ввиду, спс за данные