diff --git a/core/modules/migrate/migrate.api.php b/core/modules/migrate/migrate.api.php
index 7cfb9b7..8aa68d5 100644
--- a/core/modules/migrate/migrate.api.php
+++ b/core/modules/migrate/migrate.api.php
@@ -5,10 +5,6 @@
  * Hooks provided by the Migrate module.
  */
 
-use Drupal\migrate\Entity\MigrationInterface;
-use Drupal\migrate\Plugin\MigrateSourceInterface;
-use Drupal\migrate\Row;
-
 /**
  * @defgroup migration Migration API
  * @{
@@ -100,32 +96,3 @@
  * @endcode
  * @}
  */
-
-/**
- * @addtogroup hooks
- * @{
- */
-
-/**
- * Allows adding data to a row before processing it.
- *
- * For example, filter module used to store filter format settings in the
- * variables table which now needs to be inside the filter format config
- * file. So, it needs to be added here.
- *
- * hook_migrate_MIGRATION_ID_prepare_row() is also available.
- *
- * @ingroup migration
- */
-function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
-  if ($migration->id() == 'd6_filter_formats') {
-    $value = $source->getDatabase()->query('SELECT value FROM {variable} WHERE name = :name', array(':name' => 'mymodule_filter_foo_' . $row->getSourceProperty('format')))->fetchField();
-    if ($value) {
-      $row->setSourceProperty('settings:mymodule:foo', unserialize($value));
-    }
-  }
-}
-
-/**
- * @} End of "addtogroup hooks".
- */
diff --git a/core/modules/migrate/src/Entity/Migration.php b/core/modules/migrate/src/Entity/Migration.php
index cdc2216..868a486 100644
--- a/core/modules/migrate/src/Entity/Migration.php
+++ b/core/modules/migrate/src/Entity/Migration.php
@@ -14,6 +14,7 @@
 use Drupal\migrate\MigrateSkipRowException;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\migrate\Plugin\RequirementsInterface;
+use Drupal\migrate\Row;
 use Drupal\Component\Utility\NestedArray;
 
 /**
@@ -480,4 +481,12 @@ public function setTrackLastImported($track_last_imported) {
   public function getMigrationDependencies() {
     return $this->migration_dependencies;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareRow(Row $row) {
+    // We provide an empty implementation so extended classes can safely call
+    // parent::prepareRow().
+  }
 }
diff --git a/core/modules/migrate/src/Entity/MigrationInterface.php b/core/modules/migrate/src/Entity/MigrationInterface.php
index 5d20723..95b950a 100644
--- a/core/modules/migrate/src/Entity/MigrationInterface.php
+++ b/core/modules/migrate/src/Entity/MigrationInterface.php
@@ -8,6 +8,7 @@
 namespace Drupal\migrate\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\migrate\Row;
 
 /**
  * Interface for migrations.
@@ -283,4 +284,13 @@ public function setTrackLastImported($track_last_imported);
    */
   public function getMigrationDependencies();
 
+  /**
+   * Modify the source data before the processing pipeline begins, and also
+   * potentially throw MigrateIgnoreRowException to skip it entirely.
+   *
+   * @param \Drupal\migrate\Row $row
+   *
+   * @throws \Drupal\migrate\MigrateSkipRowException
+   */
+  public function prepareRow(Row $row);
 }
diff --git a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
index 9a17806..f662d6e 100644
--- a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
+++ b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php
@@ -12,6 +12,7 @@
 use Drupal\migrate\MigrateException;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\migrate\Plugin\MigrateSourceInterface;
+use Drupal\migrate\MigrateSkipRowException;
 use Drupal\migrate\Row;
 
 /**
@@ -201,13 +202,20 @@ protected function getModuleHandler() {
    * {@inheritdoc}
    */
   public function prepareRow(Row $row) {
-
-    $result = TRUE;
-    $result_hook = $this->getModuleHandler()->invokeAll('migrate_prepare_row', array($row, $this, $this->migration));
-    $result_named_hook = $this->getModuleHandler()->invokeAll('migrate_' . $this->migration->id() . '_prepare_row', array($row, $this, $this->migration));
-
-    // We're explicitly skipping this row - keep track in the map table.
-    if (($result_hook && in_array(FALSE, $result_hook)) || ($result_named_hook && in_array(FALSE, $result_named_hook))) {
+    $return = TRUE;
+    try {
+      // Give the migration class a chance to modify the source data.
+      $this->migration->prepareRow($row);
+      if ($this->trackChanges) {
+        // When tracking changed data, we want to quietly skip (rather than
+        // "ignore") rows with no changes. The caller needs to make that decision,
+        // so we need to provide them with the necessary information (before and
+        // after hashes).
+        $row->rehash();
+      }
+    }
+    catch (MigrateSkipRowException $e) {
+      // We're explicitly ignoring this row - keep track in the map table.
       // Make sure we replace any previous messages for this item with any
       // new ones.
       $id_map = $this->migration->getIdMap();
@@ -217,17 +225,12 @@ public function prepareRow(Row $row) {
       $this->numIgnored++;
       $this->currentRow = NULL;
       $this->currentSourceIds = NULL;
-      $result = FALSE;
-    }
-    elseif ($this->trackChanges) {
-      // When tracking changed data, We want to quietly skip (rather than
-      // "ignore") rows with changes. The caller needs to make that decision,
-      // so we need to provide them with the necessary information (before and
-      // after hashes).
-      $row->rehash();
+      $return = FALSE;
     }
+
     $this->numProcessed++;
-    return $result;
+
+    return $return;
   }
 
   /**
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/Block.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/Block.php
index b273641..b202948 100644
--- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/Block.php
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/Block.php
@@ -87,8 +87,8 @@ public function prepareRow(Row $row) {
       ->fetchCol();
     $row->setSourceProperty('roles', $roles);
     $settings = array();
-    // Contrib can use hook_migration_d6_block_prepare_row() to add similar
-    // variables via $migration->getSource()->variableGet().
+    // Contrib can extend prepareRow() to add similar variables via
+    // $migration->getSource()->variableGet().
     switch ($module) {
       case 'aggregator':
         list($type, $id) = explode('-', $delta);
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/FilterFormat.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/FilterFormat.php
index 62c211f..e405478 100644
--- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/FilterFormat.php
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/FilterFormat.php
@@ -64,8 +64,8 @@ public function prepareRow(Row $row) {
         'weight' => $raw_filter['weight'],
         'settings' => array(),
       );
-      // Load the filter settings for the filter module, modules can use
-      // hook_migration_d6_filter_formats_prepare_row() to add theirs.
+      // Load the filter settings for the filter module, modules can extend
+      // prepareRow() to add theirs.
       if ($raw_filter['module'] == 'filter') {
         if (!$delta) {
           if ($setting = $this->variableGet("allowed_html_$format", NULL)) {
