Пакетное выполнение операций в Drupal 7

№9-1,

технические науки

В статье рассказывается о пакетном выполнении операций в Drupal 7.

Похожие материалы

В этой статье поговорим об пакетном выполнении операций в Drupal 7. На примере двух модулей покажу как программно реализовать данный метод.

Первый модуль позволяет импортировать изображения в содержимое узла сайта в качестве значений поля. Я использовал этот модуль как замену существующего в шестой версии Drupal одноименного модуля. Этот модуль находился в составе модуля Image, который был признан устаревшим. На его замену пришёл модуль Media. По-моему скромному мнению, этот модуль недостаточно удобен в плане работы с большим количеством изображений. В отличие от своего предшественника мой модуль изображения для фотогалереи добавляет в один узел, а не в разное содержимое. Это позволяет использовать модуль Gallery formatter для вывода фотогалереи.

Второй модуль предназначен для импорта списка специальностей в термины таксономии. Задача была специфической, поэтому потребовалось написание модуля. В результате импорта получил иерахическую систему терминов таксономии. Этот модуль несложно переквалифицировать для выполнения других задач импорта в словарь таксономии.

Модуль для пакетного выполнения импорта изображений

Создадим директорию image_import для нашего модуля. Добавим информацию о модуле в файл image_import.info:

name = Image Import
description = Пакетный импорт изображений.
core = 7.x
package = Media
dependencies[] = image
configure = admin/content/image_import

Установим зависимость от модуля Image.

Добавим информацию об установке модуля в файл image_import.module:

function image_import_install() {
  $url = file_create_url(file_default_scheme() . '://import');
  if (!is_dir($url)) {
    mkdir($url);
  }    
  $url = file_create_url(file_default_scheme() . '://image');
  if (!is_dir($url)) {
    mkdir($url);
  }
}

При установке модуля создаём две директории:

  • import. Для импорта из неё файлов изображений.
  • image. В эту директорию будут перемещаться файлы изображений после импорта.

Они обе будут располагаться в корне общедоступного пути файловой системы нашего сайта.

Добавим информацию об удалении модуля в файл image_import.module:

function image_import_uninstall() {
  db_delete('variable')
    ->condition('name', 'image_import_%', 'LIKE')
    ->execute();
  cache_clear_all();
}

При удалении модуля традиционно удаляем наши записи из таблицы variable базы данных.

Также добавим в функцию image_import_install код, который создает новый тип материала и добавляет к нему поле для хранения изображений:

  $type = array(
    'type' => 'image', 
    'name' => st('Изображение'), 
    'base' => 'node_content', 
    'description' => st('Изображение'), 
    'custom' => 1, 
    'modified' => 1, 
    'locked' => 0,
  );
  $type = node_type_set_defaults($type);
  node_type_save($type);
  $field = field_info_field('field_image');
  $instance = field_info_instance('node', 'field_image', $type->type);
  if (empty($field)) {
    $field = array(
      'field_name' => 'field_image', 
      'type' => 'image', 
      'entity_types' => array('node'),
      'module' => 'image',
    );
    $field = field_create_field($field);
  }
  if (empty($instance)) {
    $instance = array(
      'field_name' => 'field_image', 
      'entity_type' => 'node', 
      'bundle' => $type->type, 
      'label' => 'Изображение',
      'required' => 1,
      'widget' => array(
        'type' => 'image_image',
        'module' => 'image',
      ), 
      'settings' => array(
        'file_extensions' => 'png gif jpg jpeg',
        'max_resolution' => '1200x1200',
      ), 
      'display' => array(
        'default' => array(
          'label' => 'hidden', 
          'type' => 'image',
          'settings' => array(
            'image_style' => 'medium',
          ),
          'module' => 'image',
        ), 
        'teaser' => array(
          'label' => 'hidden', 
          'type' => 'hidden',
        ),
      ),
    );
    $instance = field_create_instance($instance);
  }

Это предназначено для того, чтобы сразу после установки модуля мы могли начать импорт изображений в программно созданный тип материала. Значит не потребуется дополнительной настройки системы. 

Пакетное выполнение импорта изображений в Drupal 7

Для начала создадим пункт меню:

/**
 * hook_menu().
 */
function image_import_menu()  {
  $menu['admin/content/image_import'] = array(
    'title'  => 'Импорт изображений',
    'description' => 'Пакетный импорт изображений.',
    'type' => MENU_NORMAL_ITEM,
    'page callback'  => 'drupal_get_form',
    'page arguments' => array('image_import_form'),
    'access arguments' => array('import images'),
  );
  return $menu;
}

Определяем права доступа:

/**
 * hook_permission().
 */
function image_import_permission() {
  return array(
    'administer import images' => array(
      'title' => t('Настройка импорта изображений'),
      'description' => t('Настройка пакетного импорта изображений.'),
    ),
    'import images' => array(
      'title' => t('Импорт изображений'),
      'description' => t('Пакетный импорт изображений.'),
    ),
  );
}

Добавляем страницу для импорта изображений. Готовим форму для заполнения этой страницы:

function image_import_form() {
  $form['image_import_path'] = array(
    '#type' => 'textfield',
    '#title' => t('Директория импорта'),
    '#default_value' => variable_get('image_import_path', 'public://import'),
    '#description' => t('Директория, где должны располагаться изображения для импорта.'),
    '#required' => TRUE,
  );
  $form['image_import_title'] = array(
    '#type' => 'textfield',
    '#title' => t('Фотогалерея'),
    '#default_value' => variable_get('image_import_title', ''),
    '#description' => t('Название фотогалереи для импорта изображений.'),
    '#required' => TRUE,
  );
  $form['image_import_clear'] = array(
    '#type' => 'checkbox',
    '#default_value' => variable_get('image_import_clear', TRUE),
    '#title' => t('Очистить директорию после импорта'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Начать',
  );
  return $form;
}

Добавим поле для ввода директории, из которой необходио выполнять импорт изображений для импорта. Это позволит использовать любую директорию для импорта. Также нам потребуется поле для ввода названия фотогалереи. Значение этого поля будет соответствовать названию узла, в который мы будем импортировать изображения. Ещё оджно поле будет отвечать за очистку директории после импорта. Если установить этот признак, то система удалит все импортированные изображения из директории. Рекомендуется его использовать. Внизу будет кнопка для запуска импорта.

Используем функцию image_import_form_validate для проверки содержимого полей:

function image_import_form_validate($form, &$form_state) {
  $values = $form_state['values'];
  $dir = drupal_realpath($values['image_import_path']);
  if (!file_prepare_directory($dir)) {
    form_set_error('image_import_path', t('Директория импорта не существует.'));
  }
}

Имя этой функции должно состоять из имени функции для формирования формы и ключевого слова validate разделенных символом нижнего подчёркивания. Проверяем существования директории с помощью функции file_prepare_directory. Для вывода сообщения об ошибке используем form_set_error. В параметрах к которой передаём название поля и сообщение об ошибке.

Для обработки результатов и выполнения импорта используем функцию image_import_form_submit:

function image_import_form_submit($form, &$form_state) {
  ...
}

Принцип формирования имени этой функции совпадает с предыдущим примером, за исключением то, что в качестве ключевого слова используется submit.  Для этого получаем значения из переменной $form_state. Все значения этой переменной можно посмотреть, используя отладку с помощью модуля devel и функции dsm, которая находится в его составе. Потом сохраняем значения полей в таблице variable базы данных. Используем функцию variable_set

  dsm($form_state['values']);
  $dir = $form_state['values']['image_import_path'];
  variable_set('image_import_path', $dir);

Сканируем директорию импорта на выявление в ней файлов изображений. Если таких файлов не находим, то выдаем сообщение об ошибке.

  $files = file_scan_directory($dir, '/.*/');
  dsm($files);
  if (!empty($files)) {
    ...
  } else {
    drupal_set_message(t('Директория !dir не содержит файлов для импорта.', array('!dir' => $dir)), 'error');
  }

Далее пытаемся найти узел для импорта изображений.

    $query = new EntityFieldQuery();
    $entities = $query->entityCondition('entity_type', 'node')
      ->propertyCondition('type', 'image')
      ->propertyCondition('title', $title)
      ->propertyCondition('status', 1)
      ->range(0,1)
      ->execute();
    dsm($entities['node']);

Если такой узел не существует, то создаём его. Для этого создадим объект StdClass. Для него установим тип и заголовок узла. Остальные параметры установим по-умолчанию с помощью функции node_object_prepare. Сохраняем объект, используя node_save.

    if (empty($entities['node'])) {
      $node = new StdClass();
      $node->type = 'image';
      $node->language = LANGUAGE_NONE;
      node_object_prepare($node);
      $node->title = $title;
      node_save($node);
      ...
    } 

В результате мы должны получить идентификационный номер узла, который будем использовать в качестве фотогалереи.

    if (empty($entities['node'])) {
      ...
      $nid = $node->nid;
    } else {
      $nid = array_shift(array_keys($entities['node']));
    }

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

    foreach ($files as $file) {
      $operations[] = array(
        'image_import_batch_process',
        array($file, $nid),
      );
    }

Теперь установим параметры пакетного выполнения импорта.

    $batch = array(
      ...
    );
    batch_set($batch);

Информация об операциях импорта:

      'operations' => $operations,

Функция для обработки результатов пакетного выполнения:

      'finished' => 'image_import_batch_finished',

Заголовок для страницы пакетного выполнения:

      'title' => t('Импорт изображений'),

Информация о результатах выполнения после каждой операции:

      'progress_message' => 'Выполнено @current из @total.',

Сообщение об ошибке:

      'error_message' => 'Произошла ошибка.',

Запускаем процесс пакетного выполнения импорта изображений. В качестве параметра к функции указывает путь, по которому нужно вернуться после выполнения всех операций.

    batch_process('admin/content/image_import');

Добавим функцию для операции импорта изображения:

function image_import_batch_process($image, $nid, &$context) {
  ...
}

Добавляем изображения для узла:

  global $user;
  $file = new StdClass();
  $file->uid = $user->uid;
  $file->uri = $image->uri;
  $file->filename = basename($file->uri);
  $file->filemime = file_get_mimetype($file->uri);
  $file = file_copy($file, file_default_scheme() . '://image');
  if (!empty($file)) {
    $node = node_load($nid);
    $node->field_image[LANGUAGE_NONE][] = (array)$file;
    node_save($node);
  } else {
    watchdog('image_import', 'Ошибка копирования импортированного файла', array(), WATCHDOG_ERROR);
  }

Добавляем изображение в к списку всех файлов, который хранится в таблице files базы данных. Для этого используем функцию file_copy. Если копирование проходит успешно, то добавляем файл к полю для хранения изображений. Это поле мы добавили в начале статьи к типу материала. В случае ошибки добавляем информацию в журнал.

Используем переменную context для хранения информации о результатах выполнения операции пакетного выполнения импорта.

  // Эта информация будет доступна в mymodule_batch_finished
  $context['results'][] = $image->uri;
  // Сообщение выводимое после окончания текущей операции
  $context['message'] = 'Импортировано изображение <em>' . $image->filename . '</em>';

Добавим функцию для обработки результатов пакетного выполнения импорта изображений:

function image_import_batch_finished($success, $results, $operations) {
  if (variable_get('image_import_clear', FALSE)) {
    foreach ($results as $file) {
      if (!drupal_unlink($file)) {
        watchdog('image_import', 'Ошибка удаления файла !filename', array('!filename' => $file), WATCHDOG_ERROR);
      }
    }
    $dir = variable_get('image_import_path', 'public://import');
    if (!drupal_rmdir($dir)) {
      watchdog('image_import', 'Ошибка удаления директории !dir', array('!dir' => $dir), WATCHDOG_ERROR);
    }
  }  
  if ($success) {
    drupal_set_message('Завершен импорт ' . count($results) . ' изображений.');
  }
  else {
    drupal_set_message('Импорт изображений завершен с ошибками.', 'error');
  }
}

Если был установлен параметр, отвечающий за очистку директори после импорта, то выполняем очистку. Для этого перебираем список импортированных изображений и удаляем каждое из них. Затем пытаемся удалить саму директорию.

Выводим результаты.

Модуль для пакетного выполнения импорта терминов таксономии

Я привожу второй пример пакетного выполнения для того, чтобы показать разницу в процессе выполнения. Модуль image_import использует одну интерацию в каждой операции выполнения. Это упрощает процесс программирования. В свою очередь модуль taxonomy_import, о котором речь пойдет ниже, может использовать несколько файлов импорта, в каждом из которых содержится список терминов таксономии. Получается, что для каждого файла импорта необходимо использовать отдельную операцию. В каждой такой операции нужно использовать несколько интераций, соответствующих количеству терминов таксономии в каждом файле. О программной реализации такого принципа поговорим немного позже.

Создадим директорию taxonomy_import и файл taxonomy_import.info, в который добавим информацию о нашем модуле. Устанавливаем зависимость от модуля taxonomy.

name = taxonomy_import
description = Импорт терминов таксономии из файла
dependencies[] = taxonomy
core = 7.x
configure = admin/structure/taxonomy/taxonomy_import

Добавляем функцию установки модуля в файл taxonomy_import.install:

function image_import_install() {
  $url = file_create_url(file_default_scheme() . '://import');
  if (!is_dir($url)) {
    mkdir($url);
  }
}

В этом же файле добавляем функцию для удаления модуля:

function image_import_uninstall() {
  db_delete('variable')
    ->condition('name', 'taxonomy_import_%', 'LIKE')
    ->execute();
  cache_clear_all();
}

В файл taxonomy_import.module добавляем функционал для запуска пакетного выполнения импорта терминов таксономии.

/**
 * hook_menu().
 */
function taxonomy_import_menu() {
  $items['admin/structure/taxonomy/taxonomy_import'] = array(
    'title' => 'импорт терминов таксономии',
    'page callback'  => 'drupal_get_form',
    'page arguments' => array('taxonomy_import_form'),
    'access arguments' => array('administer site configuration'),
  );
  return $items;
}
/**
 * Выводим кнопку для запуска процедуры импорта.
 */
function taxonomy_import_form() {
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Начать',
  );
  return $form;
}

Пакетное выполнение импорта терминов таксономии в Drupal 7

Для запуска пакетного выполнения импорта терминов таксономии добавим функцию taxonomy_import_form_submit:

function taxonomy_import_form_submit($form, &$form_state) {
  ...
}

Проверяем директорию импорта на содержание в ней необходимых файлов:

  $files = file_scan_directory('public://import', '/.*\.txt$/');
  dpm($files);

Обрабатываем каждый файл. В первой строке должна храниться информация о словаре. Если информация соответствует формату, то создаём словарь таксономии с помощью функции taxonomy_vocabulary_save. В переменную для хранения операций пакетного выполнения устанавливаем функцию taxonomy_import_import для выполнения импорта и параметры для неё. В качестве параметров для функции используем данные о словаре и список термонов таксономии для импорта.

  foreach ($files as $key => $value) {
    $list = file($key);
    $voc = explode("\t", array_shift($list));
    dpm($voc);
    if (preg_match("/^[a-z0-9_]+$/", $voc[0]) && (strlen($voc[1]) > 0)) {
      $vocabulary = array(
        'machine_name' => $voc[0],
        'name' => $voc[1],
      );
      $vocabulary = (object) $vocabulary;
      taxonomy_vocabulary_save($vocabulary);
      dpm($vocabulary);
      $operations[] = array('taxonomy_import_import', array($vocabulary, $list));
    }
  }

Устанавливаем параметры для пакетного выполнения и запускаем его. Описание всех параметров было приведено выше, не будем на этом останавливаться.

  if (isset($operations) && (count($operations) > 0)) {
    $batch = array(
      'operations' => $operations,
      'finished' => 'taxonomy_import_import_finished',
      'title' => t('импорт терминов таксономии'),
      'progress_message' => 'Выполнено @current из @total.',
      'error_message' => 'Произошла ошибка.',
    );
    batch_set($batch);
    batch_process('admin/structure/taxonomy/taxonomy_import');
  }

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

Дополнительная функция для получения данных о иерахичной структуре терминов таксономии:

function _taxonomy_import_get_tree(&$depth, $t, $n) {
  if (($n > 0) && ($t[$n] == '00')) {
    _taxonomy_import_get_tree($depth, $t, $n-1);
  } else {
    $depth = $n;
  }
}

Дополнительная функция для установки данных о иерахичной структуре терминов таксономии:

function _taxonomy_import_set_tree(&$parent, $tid, $t, $n) {
  if (($n > 0) && ($t[$n] == '00')) {
    $parent[$n] = $tid;
    _taxonomy_import_set_tree($parent, $tid, $t, $n-1);
  }
}

Добавим функцию для выполнения операции импорта терминов таксономии. Эта функция будет выполняться каждую интерацию в каждой операции пакетного выполнения.

function taxonomy_import_import($voc, $list, &$context) {
  ...
}

В массиве context есть возможность для хранения дополнительной информации, которая будет передаваться в другие интерации. Установим начальные значения:

  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['max'] = count($list);
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['vid'] = $voc->vid;
    $context['sandbox']['list'] = $list;
    $context['sandbox']['parent'] = array();
    $context['sandbox']['parent'][0] = 0;
    $context['sandbox']['depth'] = 0;
  }

Приведу описание всех переменных из массива context, которые буду использовать в функции: 

  • $context['sandbox']['progress'. Количество выполненных интераций.
  • $context['sandbox']['max']. Общее количество интераций.
  • $context['sandbox']['vid']. Идентификационный номер словаря.
  • $context['sandbox']['list']. Список терминов таксономии.
  • $context['sandbox']['parent']. Информация о вышестоящих терминах таксономии в иерархической структуре.
  • $context['sandbox']['depth']. Уровень в иерархической структуре.
  • $context['finished']. Процент выполнения пакетного импорта.

Забираем первый элемент из списка терминов таксономии. Используем его для добавления термина таксономии в словарь.

  $title = array_shift($context['sandbox']['list']);
  if ($title) {
    $t = explode('.', substr($title, 0, 8));
    _taxonomy_import_get_tree($context['sandbox']['depth'], $t, 2);
    $depth = $context['sandbox']['depth'];
    $term = array(
      'name' => $title,
      'vid' => $context['sandbox']['vid'],
      'parent' => array($context['sandbox']['parent'][$depth]),
    );
    $term = (object) $term;
    taxonomy_term_save($term);
    _taxonomy_import_set_tree($context['sandbox']['parent'], $term->tid, $t, 2);
    $context['sandbox']['progress']++;
  }

Установим процент выполнения пакетного выполнения.

  $context['finished'] = $context['sandbox']['max'] > 0 ? $context['sandbox']['progress'] / $context['sandbox']['max'] : 1;

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

function taxonomy_import_import_finished($success, $results, $operations) {
  if ($success) {
    drupal_set_message(t('Импорт терминов таксономии завершен.'));
  }
  else {
    drupal_set_message(t('Произошла ошибка, и обработка не была завершена.'), 'error');    
  }
}