diff --git a/feeds.api.php b/feeds.api.php
index 58e167082b7eedad84bccbd69d7a577a721aa5ca..46cf90201fc56d0f2cba8af16a606b5a7cd9661a 100644
--- a/feeds.api.php
+++ b/feeds.api.php
@@ -257,7 +257,7 @@ function my_source_get_source(FeedsSource $source, FeedsParserResult $result, $k
 }
 
 /**
- * Alters mapping targets for processors.
+ * Adds mapping targets for processors.
  *
  * This hook allows additional target options to be added to the processors
  * mapping form.
@@ -265,18 +265,20 @@ function my_source_get_source(FeedsSource $source, FeedsParserResult $result, $k
  * If the key in $targets[] does not correspond to the actual key on the node
  * object ($node->key), real_target MUST be specified. See mappers/link.inc
  *
- * For an example implementation, see mappers/content.inc
+ * For an example implementation, see mappers/text.inc
  *
- * @param &$targets
- *   Array containing the targets to be offered to the user. Add to this array
- *   to expose additional options. Remove from this array to suppress options.
- *   Remove with caution.
- * @param $entity_type
+ * @param string $entity_type
  *   The entity type of the target, for instance a 'node' entity.
- * @param $bundle
- *   The bundle name for which to alter targets.
+ * @param string $bundle
+ *   The entity bundle to return targets for.
+ *
+ * @return array
+ *   Array containing the targets to be offered to the user. This function must
+ *   return an array, even an empty one.
  */
-function hook_feeds_processor_targets_alter(&$targets, $entity_type, $bundle) {
+function hook_feeds_processor_targets($entity_type, $bundle) {
+  $targets = array();
+
   if ($entity_type == 'node') {
     $targets['my_node_field'] = array(
       'name' => t('My custom node field'),
@@ -285,8 +287,8 @@ function hook_feeds_processor_targets_alter(&$targets, $entity_type, $bundle) {
 
       // Specify both summary_callback and form_callback to add a per mapping
       // configuration form.
-      'summary_callback' => 'my_module_summary_callback',
-      'form_callback' => 'my_module_form_callback',
+      'summary_callbacks' => array('my_module_summary_callback'),
+      'form_callbacks' => array('my_module_form_callback'),
     );
     $targets['my_node_field2'] = array(
       'name' => t('My Second custom node field'),
@@ -304,26 +306,56 @@ function hook_feeds_processor_targets_alter(&$targets, $entity_type, $bundle) {
       // targets.
       'optional_unique' => TRUE,
       'unique_callbacks' => array('my_module_mapper_unique'),
+
+      // Preprocess callbacks are called before the actual callback allowing you
+      // to prepare values on the entity or mapping array.
+      'preprocess_callbacks' => array('my_module_preprocess_callback'),
     );
   }
+
+  return $targets;
 }
 
 /**
- * Example callback specified in hook_feeds_processor_targets_alter().
+ * Alters the target array.
  *
- * @param $source
+ * This hook allows modifying the target array.
+ *
+ * @param array &$targets
+ *   Array containing the targets to be offered to the user. Add to this array
+ *   to expose additional options.
+ * @param string $entity_type
+ *   The entity type of the target, for instance a 'node' entity.
+ * @param string $bundle
+ *   The entity bundle to return targets for.
+ *
+ * @see hook_feeds_processor_targets()
+ */
+function hook_feeds_processor_targets_alter(array &$targets, $entity_type, $bundle) {
+  if ($entity_type == 'node' && $bundle == 'article') {
+    if (isset($targets['nid'])) {
+      $targets['nid']['unique_callbacks'][] = 'my_module_mapper_unique';
+      $targets['nid']['optional_unique'] = TRUE;
+    }
+  }
+}
+
+/**
+ * Example callback specified in hook_feeds_processor_targets().
+ *
+ * @param FeedsSource $source
  *   Field mapper source settings.
- * @param $entity
+ * @param object $entity
  *   An entity object, for instance a node object.
- * @param $target
+ * @param string $target
  *   A string identifying the target on the node.
- * @param $values
+ * @param array $values
  *   The value to populate the target with.
- * @param $mapping
+ * @param array $mapping
  *  Associative array of the mapping settings from the per mapping
  *  configuration form.
  */
-function my_module_set_target($source, $entity, $target, array $values, $mapping) {
+function my_module_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
   $entity->{$target}[$entity->language][0]['value'] = reset($values);
   if (isset($source->importer->processor->config['input_format'])) {
     $entity->{$target}[$entity->language][0]['format'] = $source->importer->processor->config['input_format'];
@@ -331,26 +363,25 @@ function my_module_set_target($source, $entity, $target, array $values, $mapping
 }
 
 /**
- * Example of the summary_callback specified in
- * hook_feeds_processor_targets_alter().
+ * Example of the summary_callback specified in hook_feeds_processor_targets().
  *
- * @param $mapping
+ * @param array $mapping
  *   Associative array of the mapping settings.
- * @param $target
+ * @param string $target
  *   Array of target settings, as defined by the processor or
  *   hook_feeds_processor_targets_alter().
- * @param $form
+ * @param array $form
  *   The whole mapping form.
- * @param $form_state
+ * @param array $form_state
  *   The form state of the mapping form.
  *
- * @return
+ * @return string
  *   Returns, as a string that may contain HTML, the summary to display while
  *   the full form isn't visible.
  *   If the return value is empty, no summary and no option to view the form
  *   will be displayed.
  */
-function my_module_summary_callback($mapping, $target, $form, $form_state) {
+function my_module_summary_callback(array $mapping, $target, array $form, array $form_state) {
   if (empty($mapping['my_setting'])) {
     return t('My setting <strong>not</strong> active');
   }
@@ -360,18 +391,17 @@ function my_module_summary_callback($mapping, $target, $form, $form_state) {
 }
 
 /**
- * Example of the form_callback specified in
- * hook_feeds_processor_targets_alter().
+ * Example of the form_callback specified in hook_feeds_processor_targets().
  *
  * The arguments are the same that my_module_summary_callback() gets.
  *
- * @return
+ * @return array
  *   The per mapping configuration form. Once the form is saved, $mapping will
  *   be populated with the form values.
  *
  * @see my_module_summary_callback()
  */
-function my_module_form_callback($mapping, $target, $form, $form_state) {
+function my_module_form_callback(array $mapping, $target, array $form, array $form_state) {
   return array(
     'my_setting' => array(
       '#type' => 'checkbox',
@@ -382,8 +412,7 @@ function my_module_form_callback($mapping, $target, $form, $form_state) {
 }
 
 /**
- * Example of the unique_callbacks specified in
- * hook_feeds_processor_targets_alter().
+ * Example of the unique_callbacks specified in hook_feeds_processor_targets().
  *
  * @param FeedsSource $source
  *   The Feed source.
@@ -396,10 +425,10 @@ function my_module_form_callback($mapping, $target, $form, $form_state) {
  * @param array $values
  *   The unique values to be checked.
  *
- * @return int
+ * @return int|null
  *   The existing entity id, or NULL if no existing entity is found.
  *
- * @see hook_feeds_processor_targets_alter()
+ * @see hook_feeds_processor_targets()
  * @see FeedsProcessor::existingEntityId()
  */
 function my_module_mapper_unique(FeedsSource $source, $entity_type, $bundle, $target, array $values) {
@@ -417,6 +446,25 @@ function my_module_mapper_unique(FeedsSource $source, $entity_type, $bundle, $ta
   }
 }
 
+/**
+ * Example of the preprocess_callbacks specified in hook_feeds_processor_targets().
+ *
+ * @param FeedsSource $source
+ *   The Feed source.
+ * @param object $entity
+ *   The entity being processed.
+ * @param array $target
+ *   The full target definition.
+ * @param array &$mapping
+ *   The mapping configuration.
+ *
+ * @see hook_feeds_processor_targets()
+ */
+function my_module_preprocess_callback(FeedsSource $source, $entity, array $target, array &$mapping) {
+  // Add in default values.
+  $mapping += array('setting_value' => TRUE);
+}
+
 /**
  * @}
  */
diff --git a/feeds.feeds.inc b/feeds.feeds.inc
new file mode 100644
index 0000000000000000000000000000000000000000..a51a7a6c52fa3ae0c25c584693bb1e697882eaba
--- /dev/null
+++ b/feeds.feeds.inc
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Feeds hooks.
+ */
+
+/**
+ * Implements hook_feeds_processor_targets().
+ */
+function feeds_feeds_processor_targets($entity_type, $bundle) {
+  // Record that we've been called.
+  // @see _feeds_feeds_processor_targets_alter()
+  $called = &drupal_static('feeds_feeds_processor_targets', FALSE);
+  $called = TRUE;
+
+  return array();
+}
+
+/**
+ * Implements hook_feeds_processor_targets_alter().
+ */
+function feeds_feeds_processor_targets_alter(array &$targets, $entity_type, $bundle) {
+  // This hook gets called last, so that we normalize the whole array.
+  feeds_normalize_targets($targets);
+}
+
+/**
+ * Normalizes the target array.
+ *
+ * @param array &$targets
+ *   The Feeds target array.
+ */
+function feeds_normalize_targets(array &$targets) {
+  static $defaults = array(
+    'description' => '',
+    'summary_callbacks' => array(),
+    'form_callbacks' => array(),
+    'preprocess_callbacks' => array(),
+    'unique_callbacks' => array(),
+  );
+
+  foreach (array_keys($targets) as $target) {
+    $targets[$target] += $defaults;
+
+    // Filter out any uncallable keys.
+    _feeds_filter_callback_arrays($targets[$target]);
+  }
+}
+
+/**
+ * Filters the callbacks of a single target array.
+ *
+ * @param array &$target
+ *   The target arary.
+ */
+function _feeds_filter_callback_arrays(array &$target) {
+  // Migrate keys summary_callback and form_callback to the new keys.
+  if (isset($target['summary_callback'])) {
+    $target['summary_callbacks'][] = $target['summary_callback'];
+  }
+  if (isset($target['form_callback'])) {
+    $target['form_callbacks'][] = $target['form_callback'];
+  }
+  unset($target['summary_callback'], $target['form_callback']);
+
+  static $callback_keys = array(
+    'summary_callbacks',
+    'form_callbacks',
+    'preprocess_callbacks',
+    'unique_callbacks',
+  );
+
+  // Filter out any incorrect callbacks. Do it here so it only has to be done
+  // once.
+  foreach ($callback_keys as $callback_key) {
+    $target[$callback_key] = array_filter($target[$callback_key], 'is_callable');
+  }
+
+  // This makes checking in FeedsProcessor::mapToTarget() simpler.
+  if (empty($target['callback']) || !is_callable($target['callback'])) {
+    unset($target['callback']);
+  }
+}
diff --git a/feeds.info b/feeds.info
index be3f52a54e31ec829f8e8dbf578810bad9b106ad..57eaabf8e9fe84031b48245805ae46f1412b931c 100644
--- a/feeds.info
+++ b/feeds.info
@@ -36,6 +36,7 @@ files[] = tests/feeds_mapper_date.test
 files[] = tests/feeds_mapper_date_multiple.test
 files[] = tests/feeds_mapper_field.test
 files[] = tests/feeds_mapper_file.test
+files[] = tests/feeds_mapper_hooks.test
 files[] = tests/feeds_mapper_link.test
 files[] = tests/feeds_mapper_path.test
 files[] = tests/feeds_mapper_profile.test
diff --git a/feeds.module b/feeds.module
index f53692654391a27583c6c7fdcac39c528da1ecc8..efbc703a163350c3bd63fc032240d147ad983227 100644
--- a/feeds.module
+++ b/feeds.module
@@ -35,6 +35,7 @@ function feeds_hook_info() {
     'feeds_after_save',
     'feeds_after_import',
     'feeds_after_clear',
+    'feeds_processor_targets',
     'feeds_processor_targets_alter',
     'feeds_parser_sources_alter',
   );
@@ -784,6 +785,36 @@ function feeds_system_info_alter(array &$info, $file, $type) {
   }
 }
 
+/**
+ * Implements hook_module_implements_alter().
+ */
+function feeds_module_implements_alter(array &$implementations, $hook) {
+  if ($hook === 'feeds_processor_targets_alter') {
+    // We need two implementations of this hook, so we add one that gets
+    // called first, and move the normal one to last.
+    $implementations = array('_feeds' => FALSE) + $implementations;
+
+    // Move normal implementation to last.
+    $group = $implementations['feeds'];
+    unset($implementations['feeds']);
+    $implementations['feeds'] = $group;
+  }
+}
+
+/**
+ * Implements hook_feeds_processor_targets_alter().
+ *
+ * @see feeds_feeds_processor_targets()
+ * @see feeds_feeds_processor_targets_alter()
+ */
+function _feeds_feeds_processor_targets_alter(array &$targets, $entity_type, $bundle) {
+  // If hook_feeds_processor_targets() hasn't been called, for instance, by
+  // older processors, invoke it ourself.
+  if (!drupal_static('feeds_feeds_processor_targets', FALSE)) {
+    $targets += module_invoke_all('feeds_processor_targets', $entity_type, $bundle);
+  }
+}
+
 /**
  * @}
  */
diff --git a/feeds_ui/feeds_ui.admin.inc b/feeds_ui/feeds_ui.admin.inc
index b174d75d81aee62cbf34690672d7b90b4991f582..f975ffe1864c2f582aff2833bb62ac807a9e14bb 100644
--- a/feeds_ui/feeds_ui.admin.inc
+++ b/feeds_ui/feeds_ui.admin.inc
@@ -634,12 +634,10 @@ function feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $targe
   }
 
   if ($form_state['mapping_settings_edit'] === $i) {
-    // Build the form.
-    if (isset($target['form_callback'])) {
-      $settings_form = call_user_func($target['form_callback'], $mapping, $target, $form, $form_state);
-    }
-    else {
-      $settings_form = array();
+    $settings_form = array();
+
+    foreach ($target['form_callbacks'] as $callback) {
+      $settings_form += call_user_func($callback, $mapping, $target, $form, $form_state);
     }
 
     // Merge in the optional unique form.
@@ -664,13 +662,15 @@ function feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $targe
   }
   else {
     // Build the summary.
-    if (isset($target['summary_callback'])) {
-      $summary = call_user_func($target['summary_callback'], $mapping, $target, $form, $form_state);
-    }
-    else {
-      $summary = '';
+    $summary = array();
+
+    foreach ($target['summary_callbacks'] as $callback) {
+      $summary[] = call_user_func($callback, $mapping, $target, $form, $form_state);
     }
 
+    // Filter out empty summary values.
+    $summary = implode('<br />', array_filter($summary));
+
     // Append the optional unique summary.
     if ($optional_unique_summary = feeds_ui_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state)) {
       $summary .= ' ' . $optional_unique_summary;
diff --git a/mappers/date.inc b/mappers/date.inc
index 061962d4b96a0fa8818d30f0b3df236886d47b93..d5ea81f8e28048947d6b3277167f2c6f7dcfa9df 100644
--- a/mappers/date.inc
+++ b/mappers/date.inc
@@ -6,13 +6,13 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsNodeProcessor::getMappingTargets().
+ * Implements hook_feeds_processor_targets().
  *
  * @todo Only provides "end date" target if field allows it.
  */
-function date_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function date_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
     $info = field_info_field($name);
     if (in_array($info['type'], array('date', 'datestamp', 'datetime'))) {
@@ -30,12 +30,14 @@ function date_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
       );
     }
   }
+
+  return $targets;
 }
 
 /**
- * Callback for setting target values.
+ * Callback for setting date values.
  */
-function date_feeds_set_target($source, $entity, $target, array $values) {
+function date_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
   list($field_name, $sub_field) = explode(':', $target, 2);
 
   $delta = 0;
diff --git a/mappers/file.inc b/mappers/file.inc
index 22735908185d5c750b023024c577565395eed42c..0c406776faedf30959d33ce74da2975970b19d80 100644
--- a/mappers/file.inc
+++ b/mappers/file.inc
@@ -7,11 +7,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsNodeProcessor::getMappingTargets()
+ * Implements hook_feeds_processor_targets().
  */
-function file_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function file_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
     $info = field_info_field($name);
 
@@ -47,16 +47,14 @@ function file_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
       }
     }
   }
+
+  return $targets;
 }
 
 /**
- * Callback for mapping. Here is where the actual mapping happens.
- *
- * When the callback is invoked, $target contains the name of the field the
- * user has decided to map to and $value contains the value of the feed item
- * element the user has picked as a source.
+ * Callback for mapping file fields.
  */
-function file_feeds_set_target($source, $entity, $target, array $values) {
+function file_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
   // Add default of uri for backwards compatibility.
   list($field_name, $sub_field) = explode(':', $target . ':uri');
   $info = field_info_field($field_name);
diff --git a/mappers/link.inc b/mappers/link.inc
index 90b5268c44ec44d07c14afc11fc6a81c9567c4ee..e9c363227a05c76c2b5d131b1260976018e1a7ed 100644
--- a/mappers/link.inc
+++ b/mappers/link.inc
@@ -6,11 +6,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsProcessor::getMappingTargets()
+ * Implements hook_feeds_processor_targets().
  */
-function link_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function link_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
     $info = field_info_field($name);
     if ($info['type'] == 'link_field') {
@@ -32,16 +32,14 @@ function link_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
       }
     }
   }
+
+  return $targets;
 }
 
 /**
- * Callback for mapping. Here is where the actual mapping happens.
- *
- * When the callback is invoked, $target contains the name of the field the
- * user has decided to map to and $value contains the value of the feed item
- * element the user has picked as a source.
+ * Callback for mapping link fields.
  */
-function link_feeds_set_target($source, $entity, $target, array $values) {
+function link_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
   list($field_name, $column) = explode(':', $target);
 
   $field = isset($entity->$field_name) ? $entity->$field_name : array('und' => array());
diff --git a/mappers/list.inc b/mappers/list.inc
index 6a8a16878442c11f9bea22a59ef1806c4cc22160..8c5dc3627f44c367170c3bc819d868fe4afb9cd4 100644
--- a/mappers/list.inc
+++ b/mappers/list.inc
@@ -6,11 +6,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsProcessor::getMappingTargets()
+ * Implements hook_feeds_processor_targets().
  */
-function list_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function list_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
     $info = field_info_field($name);
 
@@ -42,12 +42,14 @@ function list_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
         break;
     }
   }
+
+  return $targets;
 }
 
 /**
  * Callback for setting list_boolean fields.
  */
-function list_feeds_set_boolean_target(FeedsSource $source, $entity, $target, array $values, array $mapping = array()) {
+function list_feeds_set_boolean_target(FeedsSource $source, $entity, $target, array $values) {
   $field = isset($entity->$target) ? $entity->$target : array(LANGUAGE_NONE => array());
 
   foreach ($values as $value) {
diff --git a/mappers/number.inc b/mappers/number.inc
index 2b9c436858c1d7380542fafd5df6700db0931ba3..7f9a0d8e54cb8e7f3594fa155f35b0469e29e060 100644
--- a/mappers/number.inc
+++ b/mappers/number.inc
@@ -6,11 +6,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsProcessor::getMappingTargets()
+ * Implements hook_feeds_processor_targets().
  */
-function number_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function number_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   $numeric_types = array(
     'number_integer',
     'number_decimal',
@@ -27,14 +27,14 @@ function number_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_n
       );
     }
   }
+
+  return $targets;
 }
 
 /**
- * Callback for mapping numerics.
- *
- * Ensure that $value is a numeric to avoid database errors.
+ * Callback for mapping number fields.
  */
-function number_feeds_set_target($source, $entity, $target, array $values) {
+function number_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
   // Iterate over all values.
   $field = isset($entity->$target) ? $entity->$target : array('und' => array());
 
diff --git a/mappers/path.inc b/mappers/path.inc
index cd39fb10b5f076940cf15d31d2d7f388581909b4..1dceeeac48c1b372a264609c82d216b0dc410e48 100644
--- a/mappers/path.inc
+++ b/mappers/path.inc
@@ -6,11 +6,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsNodeProcessor::getMappingTargets().
+ * Implements hook_feeds_processor_targets().
  */
-function path_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function path_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   switch ($entity_type) {
     case 'node':
     case 'taxonomy_term':
@@ -19,21 +19,19 @@ function path_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
         'name' => t('Path alias'),
         'description' => t('URL path alias of the node.'),
         'callback' => 'path_feeds_set_target',
-        'summary_callback' => 'path_feeds_summary_callback',
-        'form_callback' => 'path_feeds_form_callback',
+        'summary_callbacks' => array('path_feeds_summary_callback'),
+        'form_callbacks' => array('path_feeds_form_callback'),
       );
       break;
   }
+
+  return $targets;
 }
 
 /**
- * Callback for mapping. Here is where the actual mapping happens.
- *
- * When the callback is invoked, $target contains the name of the field the
- * user has decided to map to and $value contains the value of the feed item
- * element the user has picked as a source.
+ * Callback for mapping path aliases.
  */
-function path_feeds_set_target($source, $entity, $target, array $values, $mapping) {
+function path_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
   $alias = FALSE;
   // Path alias cannot be multi-valued, so use the first non-empty value.
   foreach ($values as $value) {
@@ -68,24 +66,8 @@ function path_feeds_set_target($source, $entity, $target, array $values, $mappin
 
 /**
  * Mapping configuration summary for path.module.
- *
- * @param $mapping
- *   Associative array of the mapping settings.
- * @param $target
- *   Array of target settings, as defined by the processor or
- *   hook_feeds_processor_targets_alter().
- * @param $form
- *   The whole mapping form.
- * @param $form_state
- *   The form state of the mapping form.
- *
- * @return
- *   Returns, as a string that may contain HTML, the summary to display while
- *   the full form isn't visible.
- *   If the return value is empty, no summary and no option to view the form
- *   will be displayed.
  */
-function path_feeds_summary_callback($mapping, $target, $form, $form_state) {
+function path_feeds_summary_callback(array $mapping, $target, array $form, array $form_state) {
   if (!module_exists('pathauto')) {
     return;
   }
@@ -101,12 +83,8 @@ function path_feeds_summary_callback($mapping, $target, $form, $form_state) {
 
 /**
  * Settings form callback.
- *
- * @return
- *   The per mapping configuration form. Once the form is saved, $mapping will
- *   be populated with the form values.
  */
-function path_feeds_form_callback($mapping, $target, $form, $form_state) {
+function path_feeds_form_callback(array $mapping, $target, array $form, array $form_state) {
   return array(
     'pathauto_override' => array(
       '#type' => 'checkbox',
diff --git a/mappers/profile.inc b/mappers/profile.inc
index 00fdf3ab9a70e9841cbbf0c7d4aa70c6967a0f6d..42f8e88e1254a69d8bb3ac4c2aabbfa2f1850145 100644
--- a/mappers/profile.inc
+++ b/mappers/profile.inc
@@ -2,16 +2,17 @@
 
 /**
  * @file
- * On behalf implementation of Feeds mapping API for user profiles.
+ * On behalf implementation of Feeds mapping API for profile.module.
  */
 
 /**
- * Implements hook_feeds_processor_target_alter().
+ * Implements hook_feeds_processor_targets().
  */
-function profile_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function profile_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
 
   if ($entity_type != 'user') {
-    return;
+    return $targets;
   }
 
   $categories = profile_user_categories();
@@ -25,11 +26,13 @@ function profile_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_
       );
     }
   }
+
+  return $targets;
 }
 
 /**
  * Set the user profile target after import.
  */
-function profile_feeds_set_target($source, $entity, $target, array $values, $mapping) {
+function profile_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
   $entity->$target = reset($values);
 }
diff --git a/mappers/taxonomy.inc b/mappers/taxonomy.inc
index 75f6541c9c0e6c427b4ae24aaf91b2f2c95a8ed3..6fbdec691271337eafb906253a9904af43c0a173 100644
--- a/mappers/taxonomy.inc
+++ b/mappers/taxonomy.inc
@@ -23,7 +23,7 @@ define('FEEDS_TAXONOMY_SEARCH_TERM_GUID', 2);
 /**
  * Implements hook_feeds_parser_sources_alter().
  */
-function taxonomy_feeds_parser_sources_alter(&$sources, $content_type) {
+function taxonomy_feeds_parser_sources_alter(array &$sources, $content_type) {
   if (!empty($content_type)) {
     foreach (taxonomy_get_vocabularies($content_type) as $vocabulary) {
       $sources['parent:taxonomy:' . $vocabulary->machine_name] = array(
@@ -55,9 +55,11 @@ function taxonomy_feeds_get_source(FeedsSource $source, FeedsParserResult $resul
 }
 
 /**
- * Implements hook_feeds_processor_targets_alter().
+ * Implements hook_feeds_processor_targets().
  */
-function taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function taxonomy_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
     $info = field_info_field($name);
     if ($info['type'] == 'taxonomy_term_reference') {
@@ -65,8 +67,8 @@ function taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $bundle
         'name' => check_plain($instance['label']),
         'callback' => 'taxonomy_feeds_set_target',
         'description' => t('The @label field of the entity.', array('@label' => $instance['label'])),
-        'summary_callback' => 'taxonomy_feeds_summary_callback',
-        'form_callback' => 'taxonomy_feeds_form_callback',
+        'summary_callbacks' => array('taxonomy_feeds_summary_callback'),
+        'form_callbacks' => array('taxonomy_feeds_form_callback'),
       );
     }
   }
@@ -75,14 +77,14 @@ function taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $bundle
     $targets['tid']['description'] = t('The tid of the taxonomy term. NOTE: use this feature with care, node ids are usually assigned by Drupal.');
     unset($targets['vocabulary']);
   }
+
+  return $targets;
 }
 
 /**
- * Callback for mapping. Here is where the actual mapping happens.
- *
- * @todo Do not create new terms for non-autotag fields.
+ * Callback for mapping taxonomy terms.
  */
-function taxonomy_feeds_set_target($source, $entity, $target, array $terms, $mapping = array()) {
+function taxonomy_feeds_set_target(FeedsSource $source, $entity, $target, array $terms, array $mapping) {
   // Add in default values.
   $mapping += array(
     'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_NAME,
@@ -252,24 +254,8 @@ function taxonomy_feeds_term_lookup_term_by_guid($guid) {
 
 /**
  * Mapping configuration summary for taxonomy.module.
- *
- * @param array $mapping
- *   Associative array of the mapping settings.
- * @param array $target
- *   Array of target settings, as defined by the processor or
- *   hook_feeds_processor_targets_alter().
- * @param array $form
- *   The whole mapping form.
- * @param array $form_state
- *   The form state of the mapping form.
- *
- * @return string
- *   Returns, as a string that may contain HTML, the summary to display while
- *   the full form isn't visible.
- *   If the return value is empty, no summary and no option to view the form
- *   will be displayed.
  */
-function taxonomy_feeds_summary_callback($mapping, $target, $form, $form_state) {
+function taxonomy_feeds_summary_callback(array $mapping, $target, array $form, array $form_state) {
   $options = _taxonomy_feeds_form_callback_options();
   if (empty($mapping['term_search'])) {
     return t('Search taxonomy terms by: <strong>@search</strong>', array('@search' => $options[FEEDS_TAXONOMY_SEARCH_TERM_NAME]));
@@ -279,12 +265,8 @@ function taxonomy_feeds_summary_callback($mapping, $target, $form, $form_state)
 
 /**
  * Settings form callback.
- *
- * @return array
- *   The per mapping configuration form. Once the form is saved, $mapping will
- *   be populated with the form values.
  */
-function taxonomy_feeds_form_callback($mapping, $target, $form, $form_state) {
+function taxonomy_feeds_form_callback(array $mapping, $target, array $form, array $form_state) {
   return array(
     'term_search' => array(
       '#type' => 'select',
diff --git a/mappers/text.inc b/mappers/text.inc
index 89a03ff1645cf813ccb86690a7b3a57a357646ed..aa9c2e1fb9bab292a3dfeacb2704144131d669a6 100644
--- a/mappers/text.inc
+++ b/mappers/text.inc
@@ -6,11 +6,11 @@
  */
 
 /**
- * Implements hook_feeds_processor_targets_alter().
- *
- * @see FeedsProcessor::getMappingTargets()
+ * Implements hook_feeds_processor_targets().
  */
-function text_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
+function text_feeds_processor_targets($entity_type, $bundle_name) {
+  $targets = array();
+
   $text_types = array(
     'text',
     'text_long',
@@ -37,16 +37,18 @@ function text_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_nam
     }
 
     if (!empty($instance['settings']['text_processing'])) {
-      $targets[$name]['summary_callback'] = 'text_feeds_summary_callback';
-      $targets[$name]['form_callback'] = 'text_feeds_form_callback';
+      $targets[$name]['summary_callbacks'] = array('text_feeds_summary_callback');
+      $targets[$name]['form_callbacks'] = array('text_feeds_form_callback');
     }
   }
+
+  return $targets;
 }
 
 /**
  * Callback for mapping text fields.
  */
-function text_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping = array()) {
+function text_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
   list($field_name, $column) = explode(':', $target . ':value');
 
   if ($column === 'value' && isset($source->importer->processor->config['input_format'])) {
@@ -87,7 +89,7 @@ function text_feeds_set_target(FeedsSource $source, $entity, $target, array $val
  *
  * Displays which text format will be used for the text field target.
  *
- * @see text_feeds_processor_targets_alter()
+ * @see text_feeds_processor_targets()
  * @see text_feeds_form_callback()
  */
 function text_feeds_summary_callback(array $mapping, $target, array $form, array $form_state) {
@@ -109,7 +111,7 @@ function text_feeds_summary_callback(array $mapping, $target, array $form, array
  *
  * Allows to select a text format for the text field target.
  *
- * @see text_feeds_processor_targets_alter()
+ * @see text_feeds_processor_targets()
  * @see text_feeds_summary_callback()
  */
 function text_feeds_form_callback(array $mapping, $target, array $form, array $form_state) {
diff --git a/plugins/FeedsNodeProcessor.inc b/plugins/FeedsNodeProcessor.inc
index ec23016fbdf86af7bc9cb9a0c7e341d1b473e063..2cfdf3061195f345bfff51fb89908ee019852fe6 100644
--- a/plugins/FeedsNodeProcessor.inc
+++ b/plugins/FeedsNodeProcessor.inc
@@ -339,11 +339,7 @@ class FeedsNodeProcessor extends FeedsProcessor {
       );
     }
 
-    // Let other modules expose mapping targets.
-    self::loadMappers();
-    $entity_type = $this->entityType();
-    $bundle = $this->bundle();
-    drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
+    $this->getHookTargets($targets);
 
     return $targets;
   }
diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc
index fe7c541f1e3fce758bdef444dc289de99711e695..0ab04a045d4bdf21a2c56ca5261d6c9c1069dd28 100644
--- a/plugins/FeedsProcessor.inc
+++ b/plugins/FeedsProcessor.inc
@@ -585,6 +585,47 @@ abstract class FeedsProcessor extends FeedsPlugin {
     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();
   }
 
+  /**
+   * 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];
+  }
+
   /**
    * Execute mapping on an item.
    *
@@ -603,23 +644,12 @@ abstract class FeedsProcessor extends FeedsPlugin {
    * @ingroup mappingapi
    *
    * @see hook_feeds_parser_sources_alter()
-   * @see hook_feeds_data_processor_targets_alter()
-   * @see hook_feeds_node_processor_targets_alter()
-   * @see hook_feeds_term_processor_targets_alter()
-   * @see hook_feeds_user_processor_targets_alter()
+   * @see hook_feeds_processor_targets()
+   * @see hook_feeds_processor_targets_alter()
    */
   protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
+    $targets = $this->getCachedTargets();
 
-    // Static cache $targets as getMappingTargets() may be an expensive method.
-    static $sources;
-    if (!isset($sources[$this->id])) {
-      $sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
-    }
-    static $targets;
-    if (!isset($targets[$this->id])) {
-      $targets[$this->id] = $this->getMappingTargets();
-    }
-    $parser = feeds_importer($this->id)->parser;
     if (empty($target_item)) {
       $target_item = array();
     }
@@ -628,57 +658,87 @@ abstract class FeedsProcessor extends FeedsPlugin {
     // 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[$this->id][$mapping['target']]['real_target'])) {
-        $target_item->{$targets[$this->id][$mapping['target']]['real_target']} = NULL;
+      if (isset($targets[$mapping['target']]['real_target'])) {
+        $target_item->{$targets[$mapping['target']]['real_target']} = NULL;
       }
       else {
         $target_item->{$mapping['target']} = NULL;
       }
     }
 
-    /*
-    This is where the actual mapping happens: For every mapping we envoke
-    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.
-
-    If the mapping specifies a callback method, use the callback instead of
-    setTargetElement().
-    */
-    self::loadMappers();
+    // 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->config['mappings'] as $mapping) {
-      // Retrieve source element's value from parser.
-      if (isset($sources[$this->id][$mapping['source']]) &&
-          is_array($sources[$this->id][$mapping['source']]) &&
-          isset($sources[$this->id][$mapping['source']]['callback']) &&
-          function_exists($sources[$this->id][$mapping['source']]['callback'])) {
-        $callback = $sources[$this->id][$mapping['source']]['callback'];
-        $value = $callback($source, $result, $mapping['source']);
-      }
-      else {
-        $value = $parser->getSourceElement($source, $result, $mapping['source']);
-      }
+      $value = $this->getSourceValue($source, $result, $mapping['source']);
 
-      // Map the source element's value to the target.
-      if (isset($targets[$this->id][$mapping['target']]) &&
-          is_array($targets[$this->id][$mapping['target']]) &&
-          isset($targets[$this->id][$mapping['target']]['callback']) &&
-          function_exists($targets[$this->id][$mapping['target']]['callback'])) {
-        $callback = $targets[$this->id][$mapping['target']]['callback'];
+      $this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
+    }
 
-        // All target callbacks expect an array.
-        if (!is_array($value)) {
-          $value = array($value);
-        }
+    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();
 
-        $callback($source, $target_item, $mapping['target'], $value, $mapping);
+    if (isset($targets[$target]['preprocess_callbacks'])) {
+      foreach ($targets[$target]['preprocess_callbacks'] as $callback) {
+        call_user_func_array($callback, array($source, $target_item, $target, &$mapping));
       }
-      else {
-        $this->setTargetElement($source, $target_item, $mapping['target'], $value, $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);
     }
 
-    return $target_item;
+    else {
+      $this->setTargetElement($source, $target_item, $target, $value, $mapping);
+    }
   }
 
   /**
@@ -821,6 +881,22 @@ abstract class FeedsProcessor extends FeedsPlugin {
     );
   }
 
+  /**
+   * 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);
+  }
+
   /**
    * Set a concrete target element. Invoked from FeedsProcessor::map().
    *
@@ -832,6 +908,7 @@ abstract class FeedsProcessor extends FeedsPlugin {
       case 'guid':
         $target_item->feeds_item->$target_element = $value;
         break;
+
       default:
         $target_item->$target_element = $value;
         break;
@@ -852,10 +929,7 @@ abstract class FeedsProcessor extends FeedsPlugin {
    *   The serial id of an entity if found, 0 otherwise.
    */
   protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
-    $targets = &drupal_static('FeedsProcessor::existingEntityId', array());
-    if (!isset($targets[$this->id])) {
-      $targets[$this->id] = $this->getMappingTargets();
-    }
+    $targets = $this->getCachedTargets();
 
     $entity_id = 0;
 
@@ -873,13 +947,13 @@ abstract class FeedsProcessor extends FeedsPlugin {
           ->fetchField();
       }
 
-      if (!$entity_id && !empty($targets[$this->id][$target]['unique_callbacks'])) {
+      if (!$entity_id && !empty($targets[$target]['unique_callbacks'])) {
         if (!is_array($value)) {
           $value = array($value);
         }
 
-        foreach ($targets[$this->id][$target]['unique_callbacks'] as $callback) {
-          if (is_callable($callback) && $entity_id = call_user_func_array($callback, array($source, $this->entityType(), $this->bundle(), $target, $value))) {
+        foreach ($targets[$target]['unique_callbacks'] as $callback) {
+          if ($entity_id = call_user_func($callback, $source, $this->entityType(), $this->bundle(), $target, $value)) {
             // Stop at the first unique ID returned by a callback.
             break;
           }
diff --git a/plugins/FeedsTermProcessor.inc b/plugins/FeedsTermProcessor.inc
index 40b92072a301d0bd216e0bd7eb4f91141554d12a..f69819f30f901792726fb01b8dc17b212c77a981 100644
--- a/plugins/FeedsTermProcessor.inc
+++ b/plugins/FeedsTermProcessor.inc
@@ -177,21 +177,13 @@ class FeedsTermProcessor extends FeedsProcessor {
       'description' => array(
         'name' => t('Term description'),
         'description' => t('Description of the taxonomy term.'),
-        'summary_callback' => 'text_feeds_summary_callback',
-        'form_callback' => 'text_feeds_form_callback',
+        'summary_callbacks' => array('text_feeds_summary_callback'),
+        'form_callbacks' => array('text_feeds_form_callback'),
       ),
     );
 
-    // Let implementers of hook_feeds_term_processor_targets() add their targets.
-    try {
-      self::loadMappers();
-      $entity_type = $this->entityType();
-      $bundle = $this->bundle();
-      drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
-    }
-    catch (Exception $e) {
-      // Do nothing.
-    }
+    $this->getHookTargets($targets);
+
     return $targets;
   }
 
diff --git a/plugins/FeedsUserProcessor.inc b/plugins/FeedsUserProcessor.inc
index d01db439504cddd6e93686f01383640734e60099..02607f2c90c4181959be6007aa133dd60191ff43 100644
--- a/plugins/FeedsUserProcessor.inc
+++ b/plugins/FeedsUserProcessor.inc
@@ -193,11 +193,7 @@ class FeedsUserProcessor extends FeedsProcessor {
        );
     }
 
-    // Let other modules expose mapping targets.
-    self::loadMappers();
-    $entity_type = $this->entityType();
-    $bundle = $this->bundle();
-    drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
+    $this->getHookTargets($targets);
 
     return $targets;
   }
diff --git a/tests/feeds_mapper_config.test b/tests/feeds_mapper_config.test
index 58dd11e5eacac31850928a3a12fbb68803f1be78..566bbf96d06263c4f619546ec306af066ca0089c 100644
--- a/tests/feeds_mapper_config.test
+++ b/tests/feeds_mapper_config.test
@@ -37,6 +37,8 @@ class FeedsMapperConfigTestCase extends FeedsMapperTestCase {
     // Click gear to get form.
     $this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_0');
 
+    $second_callback_value = $this->randomString();
+
     // Set some settings.
     $edit = array(
       'config[0][settings][checkbox]' => 1,
@@ -44,6 +46,7 @@ class FeedsMapperConfigTestCase extends FeedsMapperTestCase {
       'config[0][settings][textarea]' => 'Textarea value: Didery dofffffffffffffffffffffffffffffffffffff',
       'config[0][settings][radios]' => 'option1',
       'config[0][settings][select]' => 'option4',
+      'config[0][settings][second_value]' => $second_callback_value,
     );
     $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_0');
     $this->assertText(t('* Changes made to target configuration are stored temporarily. Click Save to make your changes permanent.'));
@@ -60,6 +63,7 @@ class FeedsMapperConfigTestCase extends FeedsMapperTestCase {
     $this->assertText('Textarea value: Didery dofffffffffffffffffffffffffffffffffffff');
     $this->assertText('Radios value: Option 1');
     $this->assertText('Select value: Another One');
+    $this->assertText(t('Second summary: @value', array('@value' => $second_callback_value)));
 
     // Check that settings are in db.
     $config = unserialize(db_query("SELECT config FROM {feeds_importer} WHERE id='syndication'")->fetchField());
@@ -70,6 +74,7 @@ class FeedsMapperConfigTestCase extends FeedsMapperTestCase {
     $this->assertEqual($settings['textarea'], 'Textarea value: Didery dofffffffffffffffffffffffffffffffffffff');
     $this->assertEqual($settings['radios'], 'option1');
     $this->assertEqual($settings['select'], 'option4');
+    $this->assertEqual($settings['second_value'], $second_callback_value);
 
 
     // Check that form validation works.
diff --git a/tests/feeds_mapper_hooks.test b/tests/feeds_mapper_hooks.test
new file mode 100644
index 0000000000000000000000000000000000000000..5d76043ae77178cb0c7abc1156543cafff59ee67
--- /dev/null
+++ b/tests/feeds_mapper_hooks.test
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Test case for the various callbacks implemented for mappers.
+ */
+
+/**
+ * Class for testing Feeds field mapper.
+ */
+class FeedsMapperHookTestCase extends FeedsMapperTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Mapper: Hooks and callbacks',
+      'description' => 'Test case for the various callbacks implemented for mappers.',
+      'group' => 'Feeds',
+    );
+  }
+
+  /**
+   * Basic test loading a double entry CSV file.
+   */
+  public function test() {
+
+    // Create and configure importer.
+    $this->createImporterConfiguration();
+    $this->addMappings('syndication', array(
+      0 => array(
+        'source' => 'title',
+        'target' => 'title',
+      ),
+      1 => array(
+        'source' => 'description',
+        'target' => 'test_target',
+      ),
+    ));
+
+    // Checks that alter hooks are invoked.
+    $this->assertText(t('The target description was altered.'));
+
+    // Inherently tests preprocess callbacks.
+    // @see feeds_tests_mapper_set_target()
+    $nid = $this->createFeedNode();
+    $this->drupalGet('node/2/edit');
+    $body_value = $this->xpath('//*[@name = "body[und][0][value]"]');
+    $value = unserialize((string) $body_value[0]);
+    $this->assertTrue(!empty($value));
+
+    // Tests old-style target keys.
+    $this->addMappings('syndication', array(
+      2 => array(
+        'source' => 'url',
+        'target' => 'test_target_compat',
+      ),
+    ));
+
+    // Click gear to get form.
+    $this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_2');
+
+    // Set some settings.
+    $edit = array(
+      'config[2][settings][checkbox]' => 1,
+      'config[2][settings][textfield]' => 'Some text',
+      'config[2][settings][textarea]' => 'Textarea value: Didery dofffffffffffffffffffffffffffffffffffff',
+      'config[2][settings][radios]' => 'option1',
+      'config[2][settings][select]' => 'option4',
+    );
+    $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_2');
+    $this->assertText(t('* Changes made to target configuration are stored temporarily. Click Save to make your changes permanent.'));
+  }
+
+}
diff --git a/tests/feeds_mapper_taxonomy.test b/tests/feeds_mapper_taxonomy.test
index 4a2eb8ae1a123faaea0b3b22cc7804bb373eb4aa..231235ef16671c71a3a850c9948ed62219bab29e 100644
--- a/tests/feeds_mapper_taxonomy.test
+++ b/tests/feeds_mapper_taxonomy.test
@@ -236,11 +236,13 @@ class FeedsMapperTaxonomyTestCase extends FeedsMapperTestCase {
       'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_ID,
     );
 
-    taxonomy_feeds_set_target(NULL, $entity, $target, $terms, $mapping);
+    $source = FeedsSource::instance('tmp', 0);
+
+    taxonomy_feeds_set_target($source, $entity, $target, $terms, $mapping);
     $this->assertEqual(count($entity->field_tags[LANGUAGE_NONE]), 10);
 
     // Test a second mapping with a bogus term id.
-    taxonomy_feeds_set_target(NULL, $entity, $target, array(1234), $mapping);
+    taxonomy_feeds_set_target($source, $entity, $target, array(1234), $mapping);
     $this->assertEqual(count($entity->field_tags[LANGUAGE_NONE]), 10);
   }
 
@@ -284,14 +286,16 @@ class FeedsMapperTaxonomyTestCase extends FeedsMapperTestCase {
       'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_GUID,
     );
 
-    taxonomy_feeds_set_target(NULL, $entity, $target, $guids, $mapping);
+    $source = FeedsSource::instance('tmp', 0);
+
+    taxonomy_feeds_set_target($source, $entity, $target, $guids, $mapping);
     $this->assertEqual(count($entity->field_tags[LANGUAGE_NONE]), 10);
     foreach ($entity->field_tags[LANGUAGE_NONE] as $delta => $values) {
       $this->assertEqual($tids[$delta], $values['tid'], 'Correct term id foud.');
     }
 
     // Test a second mapping with a bogus term id.
-    taxonomy_feeds_set_target(NULL, $entity, $target, array(1234), $mapping);
+    taxonomy_feeds_set_target($source, $entity, $target, array(1234), $mapping);
     $this->assertEqual(count($entity->field_tags[LANGUAGE_NONE]), 10);
     foreach ($entity->field_tags[LANGUAGE_NONE] as $delta => $values) {
       $this->assertEqual($tids[$delta], $values['tid'], 'Correct term id foud.');
diff --git a/tests/feeds_tests.module b/tests/feeds_tests.module
index fe25704746f121937e455f54a2bc13afe99d70f7..1ddd2a61ce9f398133894aa0911266b8eec5a193 100644
--- a/tests/feeds_tests.module
+++ b/tests/feeds_tests.module
@@ -102,15 +102,26 @@ function feeds_tests_files_remote() {
 }
 
 /**
- * Implements hook_feeds_processor_targets_alter().
+ * Implements hook_feeds_processor_targets().
  */
-function feeds_tests_feeds_processor_targets_alter(&$targets, $entity_type, $bundle) {
+function feeds_tests_feeds_processor_targets($entity_type, $bundle) {
+  $targets = array();
+
+  // Tests that old keys still work.
+  $targets['test_target_compat'] = array(
+    'name' => t('Old style target'),
+    'callback' => 'feeds_tests_mapper_set_target',
+    'summary_callback' => 'feeds_tests_mapper_summary',
+    'form_callback' => 'feeds_tests_mapper_form',
+  );
+
   $targets['test_target'] = array(
     'name' => t('Test Target'),
     'description' => t('This is a test target.'),
     'callback' => 'feeds_tests_mapper_set_target',
-    'summary_callback' => 'feeds_tests_mapper_summary',
-    'form_callback' => 'feeds_tests_mapper_form',
+    'summary_callbacks' => array('feeds_tests_mapper_summary', 'feeds_tests_mapper_summary_2'),
+    'form_callbacks' => array('feeds_tests_mapper_form', 'feeds_tests_mapper_form_2'),
+    'preprocess_callbacks' => array(array(new FeedsTestsPreprocess(), 'callback')),
   );
 
   $targets['test_unique_target'] = array(
@@ -119,7 +130,34 @@ function feeds_tests_feeds_processor_targets_alter(&$targets, $entity_type, $bun
     'callback' => 'feeds_tests_mapper_set_target',
     'optional_unique' => TRUE,
     'unique_callbacks' => array('feeds_tests_mapper_unique'),
+    'preprocess_callbacks' => array(
+      array('FeedsTestsPreprocess', 'callback'),
+      // Make sure that invalid callbacks are filtered.
+      '__feeds_tests_invalid_callback',
+    ),
   );
+
+  return $targets;
+}
+
+/**
+ * Implements hook_feeds_processor_targets_alter().
+ */
+function feeds_tests_feeds_processor_targets_alter(array &$targets, $entity_type, $bundle) {
+  if (!isset($targets['test_target'])) {
+    return;
+  }
+
+  $targets['test_target']['description'] = t('The target description was altered.');
+}
+
+/**
+ * Preprocess callback for test_target.
+ *
+ * @see feeds_tests_feeds_processor_targets()
+ */
+function feeds_tests_preprocess_callback(FeedsSource $source, $target_item, $target, array &$mapping) {
+  $mapping['required_value'] = TRUE;
 }
 
 /**
@@ -127,7 +165,11 @@ function feeds_tests_feeds_processor_targets_alter(&$targets, $entity_type, $bun
  *
  * @see my_module_set_target()
  */
-function feeds_tests_mapper_set_target($source, $entity, $target, $value, $mapping) {
+function feeds_tests_mapper_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  if (empty($mapping['required_value'])) {
+    trigger_error('The required value was not set.', E_USER_ERROR);
+  }
+
   $entity->body['und'][0]['value'] = serialize($mapping);
 }
 
@@ -136,7 +178,7 @@ function feeds_tests_mapper_set_target($source, $entity, $target, $value, $mappi
  *
  * @see my_module_summary_callback()
  */
-function feeds_tests_mapper_summary($mapping, $target, $form, $form_state) {
+function feeds_tests_mapper_summary(array $mapping, $target, array $form, array $form_state) {
   $options = array(
     'option1' => t('Option 1'),
     'option2' => t('Another Option'),
@@ -171,10 +213,20 @@ function feeds_tests_mapper_summary($mapping, $target, $form, $form_state) {
   return drupal_render($list);
 }
 
+/**
+ * Provides a second summary callback.
+ *
+ * @see my_module_summary_callback()
+ */
+function feeds_tests_mapper_summary_2(array $mapping, $target, array $form, array $form_state) {
+  $mapping += array('second_value' => '');
+  return t('Second summary: @value', array('@value' => $mapping['second_value']));
+}
+
 /**
  * Provides the form with mapper settings.
  */
-function feeds_tests_mapper_form($mapping, $target, $form, $form_state) {
+function feeds_tests_mapper_form(array $mapping, $target, array $form, array $form_state) {
   $mapping += array(
     'checkbox' => FALSE,
     'textfield' => '',
@@ -214,10 +266,23 @@ function feeds_tests_mapper_form($mapping, $target, $form, $form_state) {
   );
 }
 
+/**
+ * Provides a second settings form.
+ */
+function feeds_tests_mapper_form_2(array $mapping, $target, array $form, array $form_state) {
+  return array(
+    'second_value' => array(
+      '#type' => 'textfield',
+      '#title' => t('The second callback value'),
+      '#default_value' => !empty($mapping['second_value']) ? $mapping['second_value'] : '',
+    ),
+  );
+}
+
 /**
  * Callback for unique_callbacks for test_target mapper.
  *
- * @see feeds_tests_feeds_processor_targets_alter()
+ * @see feeds_tests_feeds_processor_targets()
  */
 function feeds_tests_mapper_unique(FeedsSource $source, $entity_type, $bundle, $target, array $values) {
   $query = new EntityFieldQuery();
@@ -231,3 +296,19 @@ function feeds_tests_mapper_unique(FeedsSource $source, $entity_type, $bundle, $
     return key($result[$entity_type]);
   }
 }
+
+/**
+ * Helper class to ensure callbacks can be objects.
+ */
+class FeedsTestsPreprocess {
+
+/**
+ * Preprocess callback for test_target.
+ *
+ * @see feeds_tests_feeds_processor_targets()
+ */
+  public static function callback(FeedsSource $source, $target_item, $target, array &$mapping) {
+    $mapping['required_value'] = TRUE;
+  }
+
+}