<?php // $Id$ /** * @file * Definition of FeedsDataProcessor. */ /** * Creates simple table records from feed items. Uses Data module. */ class FeedsDataProcessor extends FeedsProcessor { /** * Implementation of FeedsProcessor::process(). */ public function process(FeedsParserResult $parserResult, FeedsSource $source) { // Count number of created and updated nodes. $inserted = $updated = 0; foreach ($parserResult->value['items'] as $item) { if (!($id = $this->existingItemId($item, $source)) || $this->config['update_existing']) { // Map item to a data record, feed_nid and timestamp are mandatory. $data = array(); $data['feed_nid'] = $source->feed_nid; $data['timestamp'] = FEEDS_REQUEST_TIME; $data = $this->map($item, $data); // Save data. if ($id) { $data['id'] = $id; $this->handler()->update($data, 'id'); $updated++; } else { $this->handler()->insert($data); $inserted++; } } } // Set messages. if ($inserted) { drupal_set_message(t('Created !number records in !table.', array('!number' => $inserted, '!table' => $this->tableName()))); } elseif ($updated) { drupal_set_message(t('Updated !number records in !table.', array('!number' => $updated, '!table' => $this->tableName()))); } else { drupal_set_message(t('There are no new records.')); } } /** * Implementation of FeedsProcessor::clear(). * * Delete all data records for feed_nid in this table. */ public function clear(FeedsSource $source) { $clause = array( 'feed_nid' => $source->feed_nid, ); $num = $this->handler()->delete($clause); drupal_set_message(t('Deleted !number records from !table.', array('!number' => $num, '!table' => $this->tableName()))); } /** * Implement expire(). */ public function expire($time = NULL) { if ($time === NULL) { $time = $this->expiryTime(); } if ($time == FEEDS_EXPIRE_NEVER) { return; } $clause = array( 'timestamp' => array( '<', FEEDS_REQUEST_TIME - $time, ), ); $num = $this->handler()->delete($clause); drupal_set_message(t('Expired !number records from !table.', array('!number' => $num, '!table' => $this->tableName()))); } /** * Return expiry time. */ public function expiryTime() { return $this->config['expire']; } /** * Override parent::addMapping() and create a new field for new mapping * targets. * * @see getMappingTargets(). */ public function addMapping($source, $target, $unique = FALSE) { if (empty($source) || empty($target)) { return; } // Create a new field with targets that start with "new:" @list($new, $type) = explode(':', $target); if ($new == 'new') { // Build a field name from the source key. $field_name = data_safe_name($source); // Get the full schema spec from data. $type = data_get_field_definition($type); // Add the field to the table. $schema = $this->table()->get('table_schema'); if (!isset($schema['fields'][$field_name])) { $target = $this->table()->addField($field_name, $type); // Let the user know. drupal_set_message(t('Created new field "!name".', array('!name' => $field_name))); } else { throw new Exception(t('Field !field_name already exists as a mapping target. Remove it from mapping if you would like to map another source to it. Remove it from !data_table table if you would like to change its definition.', array('!field_name' => $field_name, '!data_table' => l($this->table()->get('name'), 'admin/content/data')))); } } // Let parent populate the mapping configuration. parent::addMapping($source, $target, $unique); } /** * Return available mapping targets. */ public function getMappingTargets() { $schema = $this->table()->get('table_schema'); $meta = $this->table()->get('meta'); // Collect all existing fields except id and field_nid and offer them as // mapping targets. $existing_fields = $new_fields = array(); if (isset($schema['fields'])) { foreach ($schema['fields'] as $field_name => $field) { if (!in_array($field_name, array('id', 'feed_nid'))) { // Any existing field can be optionally unique. $existing_fields[$field_name] = array( 'name' => empty($meta['fields'][$field_name]['label']) ? $field_name : $meta['fields'][$field_name]['label'], 'optional_unique' => TRUE, ); } } } // Do the same for every joined table. foreach ($this->handler()->joined_tables as $table) { $schema = data_get_table($table)->get('table_schema'); if (isset($schema['fields'])) { foreach ($schema['fields'] as $field_name => $field) { if (!in_array($field_name, array('id', 'feed_nid'))) { // Fields in joined tables can't be unique. $existing_fields["$table.$field_name"] = array( 'name' => $table .'.'. (empty($meta['fields'][$field_name]['label']) ? $field_name : $meta['fields'][$field_name]['label']), 'optional_unique' => FALSE, ); } } } } // Now add data field types as mapping targets. $field_types = drupal_map_assoc(array_keys(data_get_field_definitions())); foreach ($field_types as $k => $v) { $new_fields['new:'. $k] = t('[new] !type', array('!type' => $v)); } return $new_fields + $existing_fields; } /** * Set target element, bring element in a FeedsDataHandler format. */ public function setTargetElement(&$target_item, $target_element, $value) { if (strpos($target_element, '.')) { /** Add field in FeedsDataHandler format. This is the tricky part, FeedsDataHandler expects an *array* of records at #[joined_table_name]. We need to iterate over the $value that has been mapped to this element and create a record array from each of them. */ list($table, $field) = explode('.', $target_element); $values = array(); $value = is_array($value) ? $value : array($value); foreach ($value as $v) { // Create a record array. $values[] = array( $field => $v, ); } $target_item['#'. $table] = $values; } else { $target_item[$target_element] = $value; } } /** * Iterate through unique targets and try to load existing records. * Return id for the first match. */ protected function existingItemId($source_item, FeedsSource $source) { foreach ($this->uniqueTargets($source_item) as $target => $value) { if ($records = $this->handler()->load(array($target => $value))) { return $records[0]['id']; } } return 0; } /** * Override parent::configDefaults(). */ public function configDefaults() { return array( 'update_existing' => 0, 'expire' => FEEDS_EXPIRE_NEVER, // Don't expire items by default. 'mappings' => array(), ); } /** * Override parent::configForm(). */ public function configForm(&$form_state) { $form['update_existing'] = array( '#type' => 'checkbox', '#title' => t('Update existing items'), '#description' => t('Check if existing items should be updated from the feed.'), '#default_value' => $this->config['update_existing'], ); $period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 604800 * 4, 604800 * 12, 604800 * 24, 31536000), 'feeds_format_expire'); $form['expire'] = array( '#type' => 'select', '#title' => t('Expire items'), '#options' => $period, '#description' => t('Select after how much time data records should be deleted. The timestamp target value will be used for determining the item\'s age, see Mapping settings.'), '#default_value' => $this->config['expire'], ); return $form; } /** * Return the data table name for this feed. */ protected function tableName() { return 'feeds_data_'. $this->id; } /** * Return the data table for this feed. * * @throws Exception $e * Throws this exception if a table cannot be found and cannot be created. * * @todo: Make *Data module* throw exception when table can't be found or * can't be created. */ protected function table() { if ($table = data_get_table($this->tableName())) { return $table; } else { if ($table = data_create_table($this->tableName(), $this->baseSchema(), feeds_importer($this->id)->config['name'])) { return $table; } } throw new Exception(t('Could not create data table.')); } /** * Return a data handler for this table. * * Avoids a call to table() to not unnecessarily instantiate DataTable. */ protected function handler() { data_include('DataHandler'); feeds_include('FeedsDataHandler'); return FeedsDataHandler::instance($this->tableName(), 'id'); } /** * Every Feeds data table must have these elements. */ protected function baseSchema() { return array( 'fields' => array( 'feed_nid' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), 'id' => array( 'type' => 'serial', 'size' => 'normal', 'unsigned' => TRUE, 'not null' => TRUE, ), 'timestamp' => array( 'description' => 'The Unix timestamp for the data.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, ), ), 'indexes' => array( 'feed_nid' => array('feed_nid'), 'id' => array('id'), 'timestamp' => array('timestamp'), ), 'primary key' => array( '0' => 'id', ), ); } }