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;
}
git
committed
/**
* Provides a list of languages available on the site.
*
* @return array
* A keyed array of language_key => language_name (example: 'en' => 'English').
*/
public function languageOptions() {
$languages = array(
LANGUAGE_NONE => t('Language neutral'),
);
$language_list = language_list('enabled');
if (!empty($language_list[1])) {
foreach ($language_list[1] as $language) {
$languages[$language->language] = $language->name;
}
}
return $languages;
}
/**
* Create a new entity.
*
MegaChriz
committed
* @param FeedsSource $source
* The feeds source that spawns this entity.
*
MegaChriz
committed
* @return object
* A new entity object.
*/
MegaChriz
committed
protected function newEntity(FeedsSource $source) {
$entity = new stdClass();
$info = $this->entityInfo();
if (!empty($info['entity keys']['language'])) {
$entity->{$info['entity keys']['language']} = $this->entityLanguage();
}
return $entity;
}
/**
* 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) {
MegaChriz
committed
$info = $this->entityInfo();
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$entities = entity_load($this->entityType(), array($entity_id));
MegaChriz
committed
$entity = reset($entities);
}
else {
$args = array(':entity_id' => $entity_id);
$table = db_escape_table($info['base table']);
$key = db_escape_field($info['entity keys']['id']);
$entity = db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject();
MegaChriz
committed
if ($entity && !empty($info['entity keys']['language'])) {
$entity->{$info['entity keys']['language']} = $this->entityLanguage();
}
MegaChriz
committed
return $entity;
MegaChriz
committed
* Validates an entity.
megachriz
committed
* @param object $entity
* The entity to validate.
*
* @throws FeedsValidationException $e
MegaChriz
committed
* Thrown if validation fails.
MegaChriz
committed
protected function entityValidate($entity) {
$info = $this->entityInfo();
if (!empty($info['entity keys']['language'])) {
// Ensure that a valid language is always set.
$key = $info['entity keys']['language'];
git
committed
$languages = $this->languageOptions();
git
committed
if (empty($entity->$key) || !isset($languages[$entity->$key])) {
$entity->$key = $this->entityLanguage();
git
committed
}
}
// Perform field validation if entity is fieldable.
if (!empty($info['fieldable'])) {
try {
megachriz
committed
$this->validateFields($entity);
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
}
catch (FieldValidationException $e) {
$errors = array();
// Unravel the errors inside the FieldValidationException.
foreach ($e->errors as $field_name => $field_errors) {
foreach ($field_errors as $langcode => $field_item_errors) {
$errors = array_merge($errors, $this->unravelFieldValidationExceptionErrors($field_item_errors));
}
}
// Compose error message. If available, use the entity label to indicate
// which item failed. Fallback to the GUID value (if available) or else
// no indication.
$label = entity_label($this->entityType(), $entity);
if ($label || $label === '0' || $label === 0) {
$message = t("@error in item '@label':", array('@error' => $e->getMessage(), '@label' => $label));
}
elseif (!empty($entity->feeds_item->guid)) {
$message = t("@error in item '@guid':", array('@error' => $e->getMessage(), '@guid' => $entity->feeds_item->guid));
}
else {
$message = $e->getMessage();
}
// Compose the final error message and throw exception.
$message .= theme('item_list', array('items' => $errors));
throw new FeedsValidationException($message);
}
MegaChriz
committed
}
}
MegaChriz
committed
megachriz
committed
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* Validates fields of an entity.
*
* This is mostly a copy of the field_attach_validate() function. The
* difference is that field_attach_validate() validates *all* fields on an
* entity, while Feeds only validates the fields where the user mapped to.
*
* @param object $entity
* The entity for which the field to validate.
*
* @throws FieldValidationException
* If validation errors are found, a FieldValidationException is thrown. The
* 'errors' property contains the array of errors, keyed by field name,
* language and delta.
*/
protected function validateFields($entity) {
$entity_type = $this->entityType();
// Get fields for the entity type we are mapping to.
$fields = field_info_instances($entity_type, $this->bundle());
// Get targets.
$targets = $this->getCachedTargets();
$errors = array();
$null = NULL;
// Validate all fields that we are mapping to.
foreach ($this->getMappings() as $mapping) {
// Get real target name.
if (isset($targets[$mapping['target']]['real_target'])) {
$target_name = $targets[$mapping['target']]['real_target'];
}
else {
$target_name = $mapping['target'];
}
if (isset($fields[$target_name])) {
// Validate this field.
_field_invoke_default('validate', $entity_type, $entity, $errors, $null, array(
'field_name' => $target_name,
));
_field_invoke('validate', $entity_type, $entity, $errors, $null, array(
'field_name' => $target_name,
));
}
}
// Let other modules validate the entity.
// Avoid module_invoke_all() to let $errors be taken by reference.
foreach (module_implements('field_attach_validate') as $module) {
$function = $module . '_field_attach_validate';
$function($entity_type, $entity, $errors);
}
if ($errors) {
throw new FieldValidationException($errors);
}
}
/**
* Helper function to unravel error messages hidden in a FieldValidationException.
*
* @param array $field_item_errors
* The errors for a single field item.
*
* @return array
* The unraveled error messages.
*/
protected function unravelFieldValidationExceptionErrors($field_item_errors) {
$errors = array();
MegaChriz
committed
foreach ($field_item_errors as $field_item_error) {
if (isset($field_item_error['message'])) {
// Found the error message!
$errors[] = $field_item_error['message'];
}
elseif (is_array($field_item_error)) {
// Error message is hidden deeper in the tree.
$errors = array_merge($errors, $this->unravelFieldValidationExceptionErrors($field_item_error));
}
MegaChriz
committed
}
return $errors;
MegaChriz
committed
}
/**
* 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() {
megachriz
committed
$info = entity_get_info($this->entityType());
// Entity module has defined the plural label in "plural label" instead of
// "label plural". So if "plural label" is defined, this will have priority
// over "label plural".
if (isset($info['plural label'])) {
$info['label plural'] = $info['plural label'];
}
return $info;
MegaChriz
committed
/**
* Returns the current language for entities.
*
* This checks if the configuration value is valid.
*
* @return string
* The current language code.
*/
protected function entityLanguage() {
if (!module_exists('locale')) {
// language_list() may return languages even if the locale module is
// disabled. See https://www.drupal.org/node/173227 why.
// When the locale module is disabled, there are no selectable languages
// in the UI, so the content should be imported in LANGUAGE_NONE.
return LANGUAGE_NONE;
}
git
committed
$languages = $this->languageOptions();
MegaChriz
committed
git
committed
return isset($languages[$this->config['language']]) ? $this->config['language'] : LANGUAGE_NONE;
MegaChriz
committed
}
/**
* @}
*/
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);
megachriz
committed
// Allow modules to alter the entity before validating.
module_invoke_all('feeds_prevalidate', $source, $entity, $item, $entity_id);
$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(
'Created @number @entity.',
'Created @number @entities.',
array('@number' => $state->created) + $tokens
),
);
}
if ($state->updated) {
$messages[] = array(
'message' => format_plural(
'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(
'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.
megachriz
committed
if ($this->config['update_non_existent'] != FEEDS_DELETE_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 int|null $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
*/
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
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.
*
* @return int
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
}
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
/**
* 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();
// Get fields for the entity type we are mapping to.
$fields = field_info_instances($this->entityType(), $this->bundle());
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->getMappings() as $mapping) {
if (isset($targets[$mapping['target']]['real_target'])) {
$target_name = $targets[$mapping['target']]['real_target'];
}
Chris Leppanen
committed
else {
$target_name = $mapping['target'];
}
// If the target is a field empty the value for the targeted language
// only.
// In all other cases, just empty the target completely.
if (isset($fields[$target_name])) {
// Empty the target for the specified language.
$target_item->{$target_name}[$mapping['language']] = array();
}
else {
// Empty the whole target.
$target_item->{$target_name} = 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.
foreach ($this->getMappings() as $mapping) {
$value = $this->getSourceValue($source, $result, $mapping['source']);
$this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
}
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
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();
// 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.
*
* @return int
Alex Barth
committed
*/
public function expiryTime() {
return FEEDS_EXPIRE_NEVER;
}
/**
* Declare default configuration.
*
* @return array
Alex Barth
committed
*/
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,
MegaChriz
committed
'language' => LANGUAGE_NONE,
) + parent::configDefaults();
megachriz
committed
/**
* Validates the configuration.
*
* @return array
* A list of errors.
*/
public function validateConfig() {
$errors = parent::validateConfig();
$info = $this->entityInfo();
$config = $this->getConfig();
megachriz
committed
// Check configured bundle if the bundle is configurable.
if (isset($config['bundle']) && !empty($info['entity keys']['bundle'])) {
megachriz
committed
$bundles = $this->bundleOptions();