Newer
Older
Alex Barth
committed
<?php
/**
* @file
* Contains FeedsProcessor and related classes.
*/
fietserwin
committed
// Insert mode for new items.
define('FEEDS_SKIP_NEW', 0);
define('FEEDS_INSERT_NEW', 1);
// Update mode for existing items.
define('FEEDS_SKIP_EXISTING', 0);
define('FEEDS_REPLACE_EXISTING', 1);
define('FEEDS_UPDATE_EXISTING', 2);
// Options for handling content in Drupal but not in source data.
define('FEEDS_SKIP_NON_EXISTENT', 'skip');
define('FEEDS_DELETE_NON_EXISTENT', 'delete');
// Default limit for creating items on a page load, not respected by all
// processors.
define('FEEDS_PROCESS_LIMIT', 50);
/**
* Thrown if a validation fails.
*/
class FeedsValidationException extends Exception {}
/**
* Thrown if a an access check fails.
*/
class FeedsAccessException extends Exception {}
Alex Barth
committed
/**
* Abstract class, defines interface for processors.
*/
abstract class FeedsProcessor extends FeedsPlugin {
Chris Leppanen
committed
/**
* Implements FeedsPlugin::pluginType().
*/
Chris Leppanen
committed
return 'processor';
}
/**
* @defgroup entity_api_wrapper Entity API wrapper.
*/
/**
* Entity type this processor operates on.
*/
public abstract function entityType();
/**
* Bundle type this processor operates on.
*
* Defaults to the entity type for entities that do not define bundles.
*
* @return string|NULL
* The bundle type this processor operates on, or NULL if it is undefined.
*/
public function bundle() {
return $this->config['bundle'];
}
/**
* Provides a list of bundle options for use in select lists.
*
* @return array
* A keyed array of bundle => label.
*/
public function bundleOptions() {
$options = array();
foreach (field_info_bundles($this->entityType()) as $bundle => $info) {
if (!empty($info['label'])) {
$options[$bundle] = $info['label'];
}
else {
$options[$bundle] = $bundle;
}
}
return $options;
}
/**
* Create a new entity.
*
* @param $source
* The feeds source that spawns this entity.
*
* @return
* A new entity object.
*/
protected abstract function newEntity(FeedsSource $source);
/**
* Load an existing entity.
*
* @param $source
* The feeds source that spawns this entity.
* @param $entity_id
* The unique id of the entity that should be loaded.
*
* @return
* A new entity object.
*
* @todo We should be able to batch load these, if we found all of the
* existing ids first.
protected function entityLoad(FeedsSource $source, $entity_id) {
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$entities = entity_load($this->entityType(), array($entity_id));
return reset($entities);
}
$info = $this->entityInfo();
$args = array(':entity_id' => $entity_id);
$table = db_escape_table($info['base table']);
$key = db_escape_field($info['entity keys']['id']);
return db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject();
}
/**
* Validate an entity.
*
* @throws FeedsValidationException $e
* If validation fails.
*/
protected function entityValidate($entity) {}
/**
* Access check for saving an enity.
*
* @param $entity
* Entity to be saved.
*
* @throws FeedsAccessException $e
* If the access check fails.
*/
protected function entitySaveAccess($entity) {}
/**
* Save an entity.
*
* @param $entity
*/
protected abstract function entitySave($entity);
/**
* Delete a series of entities.
*
* @param $entity_ids
* Array of unique identity ids to be deleted.
*/
protected abstract function entityDeleteMultiple($entity_ids);
/**
* Wrap entity_get_info() into a method so that extending classes can override
* it and more entity information. Allowed additional keys:
*
* 'label plural' ... the plural label of an entity type.
*/
protected function entityInfo() {
return entity_get_info($this->entityType());
}
/**
* @}
*/
Alex Barth
committed
/**
Alex Barth
committed
*
* @param FeedsSource $source
* Source information about this import.
* @param FeedsParserResult $parser_result
* The result of the parsing stage.
Alex Barth
committed
*/
public function process(FeedsSource $source, FeedsParserResult $parser_result) {
$state = $source->state(FEEDS_PROCESS);
if (!isset($state->removeList) && $parser_result->items) {
$this->initEntitiesToBeRemoved($source, $state);
}
fietserwin
committed
$skip_new = $this->config['insert_new'] == FEEDS_SKIP_NEW;
$skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
while ($item = $parser_result->shiftItem()) {
// Check if this item already exists.
$entity_id = $this->existingEntityId($source, $parser_result);
// If it's included in the feed, it must not be removed on clean.
if ($entity_id) {
unset($state->removeList[$entity_id]);
}
module_invoke_all('feeds_before_update', $source, $item, $entity_id);
fietserwin
committed
// If it exists, and we are not updating, or if it does not exist, and we
// are not inserting, pass onto the next item.
if (($entity_id && $skip_existing) || (!$entity_id && $skip_new)) {
continue;
}
try {
$hash = $this->hash($item);
fietserwin
committed
$changed = $hash !== $this->getHash($entity_id);
// Do not proceed if the item exists, has not changed, and we're not
// forcing the update.
fietserwin
committed
if ($entity_id && !$changed && !$this->config['skip_hash_check']) {
continue;
}
// Load an existing entity.
$entity = $this->entityLoad($source, $entity_id);
// The feeds_item table is always updated with the info for the most
// recently processed entity. The only carryover is the entity_id.
$this->newItemInfo($entity, $source->feed_nid, $hash);
$entity->feeds_item->entity_id = $entity_id;
$entity->feeds_item->is_new = FALSE;
// Build a new entity.
else {
$entity = $this->newEntity($source);
$this->newItemInfo($entity, $source->feed_nid, $hash);
// Set property and field values.
$this->map($source, $parser_result, $entity);
$this->entityValidate($entity);
// Allow modules to alter the entity before saving.
module_invoke_all('feeds_presave', $source, $entity, $item, $entity_id);
if (module_exists('rules')) {
rules_invoke_event('feeds_import_'. $source->importer()->id, $entity);
}
// Enable modules to skip saving at all.
if (!empty($entity->feeds_item->skip)) {
continue;
}
// This will throw an exception on failure.
$this->entitySaveAccess($entity);
$this->entitySave($entity);
// Allow modules to perform operations using the saved entity data.
// $entity contains the updated entity after saving.
module_invoke_all('feeds_after_save', $source, $entity, $item, $entity_id);
// Track progress.
if (empty($entity_id)) {
$state->created++;
else {
$state->updated++;
// Something bad happened, log it.
catch (Exception $e) {
$state->failed++;
drupal_set_message($e->getMessage(), 'warning');
list($message, $arguments) = $this->createLogEntry($e, $entity, $item);
$source->log('import', $message, $arguments, WATCHDOG_ERROR);
}
// Set messages if we're done.
if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
return;
}
// Remove not included items if needed.
// It depends on the implementation of the clean() method what will happen
// to items that were no longer in the source.
$this->clean($state);
$info = $this->entityInfo();
$tokens = array(
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
);
$messages = array();
if ($state->created) {
$messages[] = array(
'message' => format_plural(
$state->created,
'Created @number @entity.',
'Created @number @entities.',
array('@number' => $state->created) + $tokens
),
);
}
if ($state->updated) {
$messages[] = array(
'message' => format_plural(
$state->updated,
'Updated @number @entity.',
'Updated @number @entities.',
array('@number' => $state->updated) + $tokens
),
if ($state->unpublished) {
$messages[] = array(
'message' => format_plural(
mikran
committed
$state->unpublished,
'Unpublished @number @entity.',
'Unpublished @number @entities.',
array('@number' => $state->unpublished) + $tokens
),
);
}
if ($state->blocked) {
$messages[] = array(
'message' => format_plural(
$state->blocked,
'Blocked @number @entity.',
'Blocked @number @entities.',
array('@number' => $state->blocked) + $tokens
),
);
}
if ($state->deleted) {
$messages[] = array(
'message' => format_plural(
$state->deleted,
'Removed @number @entity.',
'Removed @number @entities.',
array('@number' => $state->deleted) + $tokens
),
);
}
if ($state->failed) {
$messages[] = array(
'message' => format_plural(
$state->failed,
'Failed importing @number @entity.',
'Failed importing @number @entities.',
array('@number' => $state->failed) + $tokens
),
'level' => WATCHDOG_ERROR,
);
}
if (empty($messages)) {
$messages[] = array(
'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
);
}
foreach ($messages as $message) {
drupal_set_message($message['message']);
$source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
Alex Barth
committed
/**
* Initializes the list of entities to remove.
*
* This populates $state->removeList with all existing entities previously
* imported from the source.
*
* @param FeedsSource $source
* Source information about this import.
* @param FeedsState $state
* The FeedsState object for the given stage.
*/
protected function initEntitiesToBeRemoved(FeedsSource $source, FeedsState $state) {
$state->removeList = array();
// We fill it only if needed.
if ($this->config['update_non_existent'] == FEEDS_SKIP_NON_EXISTENT) {
return;
}
// Get the full list of entities for this source.
$entity_ids = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('entity_type', $this->entityType())
->condition('id', $this->id)
->condition('feed_nid', $source->feed_nid)
->condition('hash', $this->config['update_non_existent'], '<>')
->execute()
->fetchCol();
if (!$entity_ids) {
return;
}
$state->removeList = array_combine($entity_ids, $entity_ids);
}
/**
* Deletes entities which were not found during processing.
*
* @todo batch delete?
*
* @param FeedsState $state
* The FeedsState object for the given stage.
*/
protected function clean(FeedsState $state) {
// We clean only if needed.
if ($this->config['update_non_existent'] == FEEDS_SKIP_NON_EXISTENT) {
return;
}
if ($total = count($state->removeList)) {
$this->entityDeleteMultiple($state->removeList);
$state->deleted += $total;
}
}
Alex Barth
committed
/**
* Remove all stored results or stored results up to a certain time for a
* source.
Alex Barth
committed
*
* @param FeedsSource $source
* Source information for this expiry. Implementers should only delete items
* pertaining to this source. The preferred way of determining whether an
* item pertains to a certain souce is by using $source->feed_nid. It is the
* processor's responsibility to store the feed_nid of an imported item in
* the processing stage.
*/
public function clear(FeedsSource $source) {
$state = $source->state(FEEDS_PROCESS_CLEAR);
// Build base select statement.
$select = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('entity_type', $this->entityType())
->condition('id', $this->id)
->condition('feed_nid', $source->feed_nid);
// If there is no total, query it.
if (!$state->total) {
$state->total = $select->countQuery()->execute()->fetchField();
}
// Delete a batch of entities.
$entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
$this->entityDeleteMultiple($entity_ids);
// Report progress, take into account that we may not have deleted as many
// items as we have counted at first.
if ($deleted_count = count($entity_ids)) {
$state->deleted += $deleted_count;
$state->progress($state->total, $state->deleted);
}
else {
$state->progress($state->total, $state->total);
}
// Report results when done.
if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
$info = $this->entityInfo();
if ($state->deleted) {
$message = format_plural(
$state->deleted,
'Deleted @number @entity',
'Deleted @number @entities',
array(
'@number' => $state->deleted,
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
)
);
$source->log('clear', $message, array(), WATCHDOG_INFO);
drupal_set_message($message);
}
else {
drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
}
}
}
/*
* Report number of items that can be processed per call.
*
* 0 means 'unlimited'.
*
* If a number other than 0 is given, Feeds parsers that support batching
* will only deliver this limit to the processor.
*
* @see FeedsSource::getLimit()
* @see FeedsCSVParser::parse()
*/
public function getLimit() {
return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
Alex Barth
committed
/**
* Deletes feed items older than REQUEST_TIME - $time.
Alex Barth
committed
*
* Do not invoke expire on a processor directly, but use
* FeedsSource::expire() instead.
*
* @param FeedsSource $source
* The source to expire entities for.
Alex Barth
committed
*
* @param $time
* (optional) All items produced by this configuration that are older than
* REQUEST_TIME - $time should be deleted. If NULL, processor should use
* internal configuration. Defaults to NULL.
* @return float
* FEEDS_BATCH_COMPLETE if all items have been processed, a float between 0
* and 0.99* indicating progress otherwise.
*
* @see FeedsSource::expire()
Alex Barth
committed
*/
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
public function expire(FeedsSource $source, $time = NULL) {
$state = $source->state(FEEDS_PROCESS_EXPIRE);
if ($time === NULL) {
$time = $this->expiryTime();
}
if ($time == FEEDS_EXPIRE_NEVER) {
return;
}
$select = $this->expiryQuery($source, $time);
// If there is no total, query it.
if (!$state->total) {
$state->total = $select->countQuery()->execute()->fetchField();
}
// Delete a batch of entities.
$entity_ids = $select->range(0, $this->getLimit())->execute()->fetchCol();
if ($entity_ids) {
$this->entityDeleteMultiple($entity_ids);
$state->deleted += count($entity_ids);
$state->progress($state->total, $state->deleted);
}
else {
$state->progress($state->total, $state->total);
}
}
/**
* Returns a database query used to select entities to expire.
*
* Processor classes should override this method to set the age portion of the
* query.
*
* @param FeedsSource $source
* The feed source.
* @param int $time
* Delete entities older than this.
*
* @return SelectQuery
* A select query to execute.
*
* @see FeedsNodeProcessor::expiryQuery()
*/
protected function expiryQuery(FeedsSource $source, $time) {
// Build base select statement.
$info = $this->entityInfo();
$id_key = $info['entity keys']['id'];
$select = db_select($info['base table'], 'e');
$select->addField('e', $id_key);
$select->join('feeds_item', 'fi', "e.$id_key = fi.entity_id");
$select->condition('fi.entity_type', $this->entityType());
$select->condition('fi.id', $this->id);
$select->condition('fi.feed_nid', $source->feed_nid);
return $select;
Alex Barth
committed
Alex Barth
committed
/**
* Counts the number of items imported by this processor.
Alex Barth
committed
*/
public function itemCount(FeedsSource $source) {
return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(':id' => $this->id, ':entity_type' => $this->entityType(), ':feed_nid' => $source->feed_nid))->fetchField();
Alex Barth
committed
}
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
/**
* Returns a statically cached version of the target mappings.
*
* @return array
* The targets for this importer.
*/
protected function getCachedTargets() {
$targets = &drupal_static('FeedsProcessor::getCachedTargets', array());
if (!isset($targets[$this->id])) {
$targets[$this->id] = $this->getMappingTargets();
}
return $targets[$this->id];
}
/**
* Returns a statically cached version of the source mappings.
*
* @return array
* The sources for this importer.
*/
protected function getCachedSources() {
$sources = &drupal_static('FeedsProcessor::getCachedSources', array());
if (!isset($sources[$this->id])) {
$sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
if (is_array($sources[$this->id])) {
foreach ($sources[$this->id] as $source_key => $source) {
if (empty($source['callback']) || !is_callable($source['callback'])) {
unset($sources[$this->id][$source_key]['callback']);
}
}
}
}
return $sources[$this->id];
}
Alex Barth
committed
/**
* Execute mapping on an item.
*
* This method encapsulates the central mapping functionality. When an item is
* processed, it is passed through map() where the properties of $source_item
* are mapped onto $target_item following the processor's mapping
* configuration.
*
* For each mapping FeedsParser::getSourceElement() is executed to retrieve
* the source element, then FeedsProcessor::setTargetElement() is invoked
* to populate the target item properly. Alternatively a
* hook_x_targets_alter() may have specified a callback for a mapping target
* in which case the callback is asked to populate the target item instead of
* FeedsProcessor::setTargetElement().
*
* @ingroup mappingapi
*
* @see hook_feeds_processor_targets()
* @see hook_feeds_processor_targets_alter()
Alex Barth
committed
*/
protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
$targets = $this->getCachedTargets();
Alex Barth
committed
if (empty($target_item)) {
$target_item = array();
}
// Many mappers add to existing fields rather than replacing them. Hence we
// need to clear target elements of each item before mapping in case we are
// mapping on a prepopulated item such as an existing node.
foreach ($this->config['mappings'] as $mapping) {
if (isset($targets[$mapping['target']]['real_target'])) {
$target_item->{$targets[$mapping['target']]['real_target']} = NULL;
}
Chris Leppanen
committed
else {
$target_item->{$mapping['target']} = NULL;
}
}
// This is where the actual mapping happens: For every mapping we invoke
// the parser's getSourceElement() method to retrieve the value of the
// source element and pass it to the processor's setTargetElement() to stick
// it on the right place of the target item.
Alex Barth
committed
foreach ($this->config['mappings'] as $mapping) {
$value = $this->getSourceValue($source, $result, $mapping['source']);
$this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
}
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
return $target_item;
}
/**
* Returns the values from the parser, or callback.
*
* @param FeedsSource $source
* The feed source.
* @param FeedsParserResult $result
* The parser result.
* @param string $source_key
* The current key being processed.
*
* @return mixed
* A value, or a list of values.
*/
protected function getSourceValue(FeedsSource $source, FeedsParserResult $result, $source_key) {
$sources = $this->getCachedSources();
if (isset($sources[$source_key]['callback'])) {
return call_user_func($sources[$source_key]['callback'], $source, $result, $source_key);
}
return feeds_importer($this->id)->parser->getSourceElement($source, $result, $source_key);
}
/**
* Maps values onto the target item.
*
* @param FeedsSource $source
* The feed source.
* @param mixed &$target_item
* The target item to apply values into.
* @param mixed $value
* A value, or a list of values.
* @param array $mapping
* The mapping configuration.
*/
protected function mapToTarget(FeedsSource $source, $target, &$target_item, $value, array $mapping) {
$targets = $this->getCachedTargets();
if (isset($targets[$target]['preprocess_callbacks'])) {
foreach ($targets[$target]['preprocess_callbacks'] as $callback) {
call_user_func_array($callback, array($source, $target_item, $target, &$mapping));
}
}
// Map the source element's value to the target.
// If the mapping specifies a callback method, use the callback instead of
// setTargetElement().
if (isset($targets[$target]['callback'])) {
// All target callbacks expect an array.
if (!is_array($value)) {
$value = array($value);
}
call_user_func($targets[$target]['callback'], $source, $target_item, $target, $value, $mapping);
Alex Barth
committed
}
else {
$this->setTargetElement($source, $target_item, $target, $value, $mapping);
}
Alex Barth
committed
}
/**
* Per default, don't support expiry. If processor supports expiry of imported
* items, return the time after which items should be removed.
*/
public function expiryTime() {
return FEEDS_EXPIRE_NEVER;
}
/**
* Declare default configuration.
*/
public function configDefaults() {
$info = $this->entityInfo();
$bundle = NULL;
if (empty($info['entity keys']['bundle'])) {
$bundle = $this->entityType();
}
return array(
'mappings' => array(),
fietserwin
committed
'insert_new' => FEEDS_INSERT_NEW,
'update_existing' => FEEDS_SKIP_EXISTING,
'update_non_existent' => FEEDS_SKIP_NON_EXISTENT,
Chris Leppanen
committed
'input_format' => NULL,
'skip_hash_check' => FALSE,
'bundle' => $bundle,
);
}
/**
* Overrides parent::configForm().
*/
public function configForm(&$form_state) {
$info = $this->entityInfo();
$form = array();
if (!empty($info['entity keys']['bundle'])) {
$form['bundle'] = array(
'#type' => 'select',
'#options' => $this->bundleOptions(),
Chris Leppanen
committed
'#title' => !empty($info['bundle name']) ? $info['bundle name'] : t('Bundle'),
'#required' => TRUE,
'#default_value' => $this->bundle(),
);
}
else {
$form['bundle'] = array(
'#type' => 'value',
'#value' => $this->entityType(),
);
}
$tokens = array('@entities' => strtolower($info['label plural']));
fietserwin
committed
$form['insert_new'] = array(
'#type' => 'radios',
'#title' => t('Insert new @entities', $tokens),
'#description' => t('New @entities will be determined using mappings that are a "unique target".', $tokens),
'#options' => array(
FEEDS_INSERT_NEW => t('Insert new @entities', $tokens),
FEEDS_SKIP_NEW => t('Do not insert new @entities', $tokens),
),
'#default_value' => $this->config['insert_new'],
);
$form['update_existing'] = array(
'#type' => 'radios',
'#title' => t('Update existing @entities', $tokens),
'#description' =>
t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
'#options' => array(
FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
FEEDS_REPLACE_EXISTING => t('Replace existing @entities', $tokens),
FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
),
'#default_value' => $this->config['update_existing'],
);
global $user;
$formats = filter_formats($user);
foreach ($formats as $format) {
$format_options[$format->format] = $format->name;
Chris Leppanen
committed
$form['skip_hash_check'] = array(
'#type' => 'checkbox',
'#title' => t('Skip hash check'),
'#description' => t('Force update of items even if item source data did not change.'),
'#default_value' => $this->config['skip_hash_check'],
);
$form['input_format'] = array(
'#type' => 'select',
'#title' => t('Text format'),
'#description' => t('Select the default input format for the text fields of the nodes to be created.'),
'#options' => $format_options,
'#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
'#required' => TRUE,
);
$form['update_non_existent'] = array(
'#type' => 'radios',
'#title' => t('Action to take when previously imported @entities are missing in the feed', $tokens),
'#description' => t('Select how @entities previously imported and now missing in the feed should be updated.', $tokens),
'#options' => array(
FEEDS_SKIP_NON_EXISTENT => t('Skip non-existent @entities', $tokens),
FEEDS_DELETE_NON_EXISTENT => t('Delete non-existent @entities', $tokens),
),
'#default_value' => $this->config['update_non_existent'],
);
Alex Barth
committed
}
/**
* Get mappings.
*/
public function getMappings() {
return isset($this->config['mappings']) ? $this->config['mappings'] : array();
}
/**
* Declare possible mapping targets that this processor exposes.
*
* @ingroup mappingapi
Alex Barth
committed
*
* @return
* An array of mapping targets. Keys are paths to targets
* separated by ->, values are TRUE if target can be unique,
* FALSE otherwise.
*/
public function getMappingTargets() {
Chris Leppanen
committed
// The bundle has not been selected.
if (!$this->bundle()) {
$info = $this->entityInfo();
$bundle_name = !empty($info['bundle name']) ? drupal_strtolower($info['bundle name']) : t('bundle');
$plugin_key = feeds_importer($this->id)->config['processor']['plugin_key'];
$url = url('admin/structure/feeds/' . $this->id . '/settings/' . $plugin_key);
drupal_set_message(t('Please <a href="@url">select a @bundle_name</a>.', array('@url' => $url, '@bundle_name' => $bundle_name)), 'warning', FALSE);
}
return array(
'url' => array(
'name' => t('URL'),
'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
'guid' => array(
'name' => t('GUID'),
'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
);
Alex Barth
committed
}
/**
* Allows other modules to expose targets.
*
* @param array &$targets
* The existing target array.
*/
protected function getHookTargets(array &$targets) {
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->bundle();
$targets += module_invoke_all('feeds_processor_targets', $entity_type, $bundle);
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
}
Alex Barth
committed
/**
* Set a concrete target element. Invoked from FeedsProcessor::map().
*
* @ingroup mappingapi
Alex Barth
committed
*/
public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
switch ($target_element) {
case 'url':
case 'guid':
$target_item->feeds_item->$target_element = $value;
break;
default:
$target_item->$target_element = $value;
break;
}
Alex Barth
committed
}
/**
* Retrieve the target entity's existing id if available. Otherwise return 0.
Alex Barth
committed
*
Alex Barth
committed
* @param FeedsSource $source
* The source information about this import.
Chris Leppanen
committed
* @param FeedsParserResult $result
Chris Leppanen
committed
* @return int
* The serial id of an entity if found, 0 otherwise.
Alex Barth
committed
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
$targets = $this->getCachedTargets();
Chris Leppanen
committed
$entity_id = 0;
// Iterate through all unique targets and test whether they already exist in
// the database.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
Chris Leppanen
committed
if ($target === 'guid' || $target === 'url') {
ekes
committed
$entity_id = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('feed_nid', $source->feed_nid)
->condition('entity_type', $this->entityType())
->condition('id', $source->id)
->condition($target, $value)
->execute()
->fetchField();
Chris Leppanen
committed
if (!$entity_id && !empty($targets[$target]['unique_callbacks'])) {
Chris Leppanen
committed
if (!is_array($value)) {
$value = array($value);
}
foreach ($targets[$target]['unique_callbacks'] as $callback) {
if ($entity_id = call_user_func($callback, $source, $this->entityType(), $this->bundle(), $target, $value)) {
Chris Leppanen
committed
// Stop at the first unique ID returned by a callback.
break;
}
}
}
// Return with the content id found.
if ($entity_id) {
Chris Leppanen
committed
return $entity_id;
Alex Barth
committed
}
Alex Barth
committed
/**
* Utility function that iterates over a target array and retrieves all
* sources that are unique.
*
* @param $batch
* A FeedsImportBatch.
Alex Barth
committed
*
* @return
* An array where the keys are target field names and the values are the
* elements from the source item mapped to these targets.
*/
protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
Alex Barth
committed
$parser = feeds_importer($this->id)->parser;
$targets = array();
foreach ($this->config['mappings'] as $mapping) {
if (!empty($mapping['unique'])) {
Alex Barth
committed
// Invoke the parser's getSourceElement to retrieve the value for this
// mapping's source.