Drupal: пишем свой парсер для Feeds

Модуль Feeds является очень популярным среди Drupal-разработчиков. Но возникает вопрос, что делать если необходимо несколько расширить его функциональность. В этом нам поможет система плагинов модуля Feeds.
Существует 3 вида плагинов от которых необходимо наследовать новые:
  • FeedsFetcher — плагин сборщика. Cтандартные: HTTP и File Upload. С помощью этого типа плагинов можно добавить новый источник данных.
  • FeedsProcessor — плагин обработки сущностей. Cтандартные: Node processor, Taxonomy term processor, User processor. С помощью нового плагина можно добавить новый обработчик, который будет создавать особенные сущности, не вписывающиеся в стандартный набор.
  • FeedsParser — плагин парсера. Стандартные парсеры в Feeds это XML, CSV и многие другие.

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

Создание модуля

Как и обычно для создания модуля нам нужно создать info файл и файл модуля:

json_example_parser.info
name = Json example parser
description = Simple json parser
package = Feeds
core = 7.x
dependencies[] = feeds


json_example_parser.module
<?php
/**
 * @file
 * Json example parser - simple parser plugin
 */

/**
 * Implementation of hook_feeds_plugins().
 * Регистрация плагина в системе.
 */
function json_example_parser_feeds_plugins() {
  $info = array();
  $info['JsonExampleParser'] = array(
    'name' => 'JSON parser',
    'description' => 'Parses custom JSON.',
    'handler' => array(
      'parent' => 'FeedsParser', // родительский класс от которого наследуем парсер, стандартные классы Feeds - FeedsFetcher, FeedsParser и FeedsProcessor
      'class' => 'JsonExampleParser', // название парсера, должно совпадать с ключом в массиве
      'file' => 'JsonExampleParser.inc', // файл класса парсера
      'path' => drupal_get_path('module', 'json_example_parser'), // путь к классу парсера
    ),
  );
  return $info;
}

// очистка кеша плагинов Feeds при включении модуля
function json_example_parser_enable() {
  //clear the cache to display in Feeds as available plugin.
  cache_clear_all('plugins:feeds:plugins', 'cache');
}
?>

В комментариях описаны основных моменты хуков.

Исходные данные

Для примера за импортируемый материал возьмем ноду “Компьютерная игра” со следующими полями:

Данные, которые будем парсить, будут приходить в JSON формате. Не важно будет это загружаемый файл либо HTTP сборщик, за это отвечает Fetcher и способы получения данных зависят от набора плагинов для сборщика.

Входной JSON:

[
  { "name":"Team Fortress 2",
    "year":2007,
    "price":0
  },
  { "name":"Warcraft III: The Frozen Throne",
    "year":2003,
    "price":13.9
  },
  { "name":"Diablo III",
    "year":2012,
    "price":33
  }
]


Создание класса парсера

Для того чтобы создать класс парсера его нужно наследовать от стандартного абстрактного класса FeedsParser и переопределить методы parse и getMappingSources. Переопределение остальных методов не является обязательным.

JsonExampleParser.inc
<?php
/**
 * A JSON example parser
 */
class JsonExampleParser extends FeedsParser {
  /**
   * Implements FeedsParser::parse().
   */
  public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
    $result = new FeedsParserResult();
    // получаем последние настройки
    $source_config = $source->getConfigFor($this);
    // извлекаем JSON данные
    $fetch_items = json_decode($fetcher_result->getRaw());
    foreach ($fetch_items as $value) {
      $item = array('name' => $value->name);
      if ($value->year) {
        $item['year'] = intval($value->year);
      }
      if ($value->price) {
        $item['price'] = floatval($value->price);
      }
      // применение настроек из формы
      if (  $source_config['type'] == 'all' ||
            ($source_config['type'] == 'free' && $item['price'] == 0) ||
            ($source_config['type'] == 'paid' && $item['price'] > 0)) {
        // добавляем запись для импорта
        $result->items[] = $item;
      }
    }
    return $result;
  }

  /**
   * Implements FeedsParser::getMappingSources().
   */
  public function getMappingSources() {
    return array(
      'name' => array(
        'name' => t('Game name'),
        'description' => t('The name of the computer game.'),
      ),
      'year' => array(
        'name' => t('Release year'),
        'description' => t('Release year of the computer game.'),
      ),
      'price' => array(
        'name' => t('Game price'),
        'description' => t('The cost of the computer game.'),
      ),
    ) + parent::getMappingSources();
  }

  /**
   * Configuration form.
   * Конфигурационная форма, которая будет отображаться на странице настройки импортера.
   */
  public function configForm(&$form_state) {
    $form = array();
    $form['type'] = array(
      '#type' => 'select',
      '#title' => t('Game type'),
      '#description' => t('Game filter by type.'),
      '#options' => array(
        'all' => t('All game'),
        'paid' => t('Paid'),
        'free' => t('Free'),
      ),
      '#default_value' => $this->config['type'],
    );
    return $form;
  }

  /**
   * Define default configuration values.
   * Стандартные настройки для парсера, которые будут применены если форма открыта впервые.
   */
  public function configDefaults() {
    return array(
      'type' => 'all',
    );
  }

  /**
   * Define defaults.
   * Определение настроек отправленных из формы.
   */
  public function sourceDefaults() {
    return array(
      'type' => $this->config['type'],
    );
  }

  /**
   * Show configuration form for users.
   * Конфигурационная форма которая будет отображаться пользователям на странице импорта.
   */
  public function sourceForm($source_config) {
    $form = array();
    $form['#weight'] = -10;
    $form['help']['#markup'] = '<div class="help"><p>' . t('Select the type of game you want to import') . ':</p></div>';
    $form['type'] = array(
      '#type' => 'select',
      '#title' => t('Game type'),
      '#description' => t('Game filter by type.'),
      '#options' => array(
        'all' => t('All game'),
        'paid' => t('Paid'),
        'free' => t('Free'),
      ),
      '#default_value' => isset($source_config['type']) ? $source_config['type'] : 'all',
    );
    return $form;
  }
}


Немного о методах.

parse — метод парсинга, получает объект класса источника FeedsSource и объект класса FeedsFetcherResult из которого извлекаются считанные данные. Данный метод формирует готовый объект FeedsParserResult с набором сущностей items для сохранения.

getMappingSources — метод который определяет поля которые будут доступны из источника для записи в поля создаваемого объекта. Например в данном случае поле маппинга name будет записываться в заголовок ноды и т.д.

configForm — этот метод предоставляет форму настройки, которая будет отображаться на странице администратора, в настройках импортера. Сохранение данных происходит автоматически.

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

sourceDefaults — переопределенный метод для получения доступа к сохраненным параметрам из формы конфигурации.

sourceForm — форма которая будет доступна пользователям при импорте, дополняя форму Fetcher’a.

Заключение

В результате мы получили полноценный плагин для модуля Feeds с неограниченными возможностями кастомизации настроек и обработки любых входных данных. Для более детального представление о плагинах Feeds советую посмотреть исходные коды стандартных дополнений, которые идут вместе с модулем.
  • +5
  • 11,3k
  • 3
Поделиться публикацией
Комментарии 3
    0
    Feeds интересен, но как ни пытался его присобачить для своих нужд, написания своих парсеров выходило быстрее.
    Немного офтоп — альтернатива для feeds — модуль ParserParser 2). Да, автора модуля Parser, как и меня, feeds не вдохновил )
      0
      А вот меня очень вдохновил. До своего парсера дело не дошло, обошёлся RSS, но используя хуки Feeds присобачил очень даже хитрый план по распределению скаченного контента по нодам.
      Ежедневно Feeds вытаскивает из твиттера и новостей Гугл по 3000 сообщений которые распределяются между 60000 страниц.
      Гугл очень любит просматривать эти страницы.
        0
        А написание процессора? Точнее модификация существующего где нибудь описана? Было бы очень интересно.

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

        Самое читаемое