diff --git a/src/Entity/Index.php b/src/Entity/Index.php
index 3f910de..6795366 100644
--- a/src/Entity/Index.php
+++ b/src/Entity/Index.php
@@ -263,6 +263,13 @@ class Index extends ConfigEntityBase implements IndexInterface {
   protected $processorInstances;
 
   /**
+   * Cached property definitions, keyed by datasource ID and property name.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface[][]
+   */
+  protected $properties = array();
+
+  /**
    * Whether reindexing has been triggered for this index in this page request.
    *
    * @var bool
@@ -597,6 +604,10 @@ public function addProcessor(ProcessorInterface $processor) {
     }
     $this->processorInstances[$processor->getPluginId()] = $processor;
 
+    if ($processor->supportsStage(ProcessorInterface::STAGE_ADD_PROPERTIES)) {
+      $this->properties = array();
+    }
+
     return $this;
   }
 
@@ -609,7 +620,14 @@ public function removeProcessor($processor_id) {
     if ($this->processorInstances === NULL) {
       $this->getProcessors();
     }
-    unset($this->processorInstances[$processor_id]);
+
+    if (!empty($this->processorInstances[$processor_id])) {
+      $processor = $this->processorInstances[$processor_id];
+      if ($processor->supportsStage(ProcessorInterface::STAGE_ADD_PROPERTIES)) {
+        $this->properties = array();
+      }
+      unset($this->processorInstances[$processor_id]);
+    }
 
     return $this;
   }
@@ -790,22 +808,31 @@ public function getFulltextFields() {
   /**
    * {@inheritdoc}
    */
-  public function getPropertyDefinitions($datasource_id, $alter = TRUE) {
-    $alter = $alter ? 1 : 0;
-    if (isset($datasource_id)) {
-      $datasource = $this->getDatasource($datasource_id);
-      $properties[$datasource_id][$alter] = $datasource->getPropertyDefinitions();
-    }
-    else {
-      $datasource = NULL;
-      $properties[$datasource_id][$alter] = array();
-    }
-    if ($alter) {
-      foreach ($this->getProcessors() as $processor) {
-        $processor->alterPropertyDefinitions($properties[$datasource_id][$alter], $datasource);
+  public function getPropertyDefinitions($datasource_id) {
+    static $recursion = FALSE;
+
+    if (!isset($this->properties[$datasource_id])) {
+      if (isset($datasource_id)) {
+        $datasource = $this->getDatasource($datasource_id);
+        $this->properties[$datasource_id] = $datasource->getPropertyDefinitions();
+      }
+      else {
+        $datasource = NULL;
+        $this->properties[$datasource_id] = array();
+      }
+
+      // We have to take care that we don't end up in an infinite loop if any
+      // processor's properties depend on the available properties on the index.
+      if (!$recursion) {
+        $recursion = TRUE;
+        foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES) as $processor) {
+          $this->properties[$datasource_id] += $processor->getPropertyDefinitions($datasource);
+        }
+        $recursion = FALSE;
       }
     }
-    return $properties[$datasource_id][$alter];
+
+    return $this->properties[$datasource_id];
   }
 
   /**
@@ -885,6 +912,7 @@ public function indexItems($limit = '-1', $datasource_id = NULL) {
         }
         catch (SearchApiException $e) {
           $variables['%index'] = $this->label();
+          debug($e->getMessage());
           watchdog_exception('search_api', $e, '%type while trying to index items on index %index: @message in %function (line %line of %file)', $variables);
         }
       }
diff --git a/src/Form/IndexProcessorsForm.php b/src/Form/IndexProcessorsForm.php
index 4f3585e..af3c61a 100644
--- a/src/Form/IndexProcessorsForm.php
+++ b/src/Form/IndexProcessorsForm.php
@@ -275,7 +275,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
         'plugin_id' => $processor_id,
         'settings' => array(),
       );
-      $processor_values = $values['processors'][$processor_id];
       if (isset($form['settings'][$processor_id])) {
         $sub_keys = array('processors', $processor_id, 'settings');
         $processor_form_state = new SubFormState($form_state, $sub_keys);
@@ -283,8 +282,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
         $new_settings[$processor_id]['settings'] = $processor->getConfiguration();
         $new_settings[$processor_id]['settings'] += array('index' => $this->entity);
       }
-      if (!empty($processor_values['weights'])) {
-        $new_settings[$processor_id]['settings']['weights'] = $processor_values['weights'];
+      if (!empty($values['processors'][$processor_id]['weights'])) {
+        $new_settings[$processor_id]['settings']['weights'] = $values['processors'][$processor_id]['weights'];
       }
     }
 
diff --git a/src/IndexInterface.php b/src/IndexInterface.php
index b4be269..417a303 100644
--- a/src/IndexInterface.php
+++ b/src/IndexInterface.php
@@ -446,9 +446,6 @@ public function getFulltextFields();
    * @param string|null $datasource_id
    *   The ID of the datasource for which the properties should be retrieved. Or
    *   NULL to retrieve all datasource-independent properties.
-   * @param bool $alter
-   *   (optional) Whether to pass the property definitions to the index's
-   *   enabled processors for altering before returning them.
    *
    * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
    *   The properties belonging to the given datasource that are available in
@@ -458,7 +455,7 @@ public function getFulltextFields();
    *   Thrown if the specified datasource isn't enabled for this index, or
    *   couldn't be loaded.
    */
-  public function getPropertyDefinitions($datasource_id, $alter = TRUE);
+  public function getPropertyDefinitions($datasource_id);
 
   /**
    * Loads a single search object of this index.
diff --git a/src/Item/Item.php b/src/Item/Item.php
index 13afec2..1f2f4b9 100644
--- a/src/Item/Item.php
+++ b/src/Item/Item.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Processor\ProcessorInterface;
+use Drupal\search_api\Processor\ProcessorPropertyInterface;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Utility;
@@ -209,6 +211,7 @@ public function getFields($extract = TRUE) {
       $data_type_fallback_mapping = Utility::getDataTypeFallbackMapping($this->index);
       foreach (array(NULL, $this->getDatasourceId()) as $datasource_id) {
         $fields_by_property_path = array();
+        $processors_with_fields = array();
         foreach ($this->index->getFieldsByDatasource($datasource_id) as $field_id => $field) {
           // Don't overwrite fields that were previously set.
           if (empty($this->fields[$field_id])) {
@@ -221,19 +224,33 @@ public function getFields($extract = TRUE) {
               $this->fields[$field_id]->setType($data_type_fallback_mapping[$field_data_type]);
             }
 
-            $fields_by_property_path[$field->getPropertyPath()][] = $this->fields[$field_id];
+            $property = $field->getDataDefinition();
+            if ($property instanceof ProcessorPropertyInterface) {
+              $processors_with_fields[$property->getProcessorId()] = TRUE;
+            }
+            elseif ($datasource_id) {
+              $fields_by_property_path[$field->getPropertyPath()][] = $this->fields[$field_id];
+            }
           }
         }
-        if ($datasource_id && $fields_by_property_path) {
-          try {
+        try {
+          if ($fields_by_property_path) {
             Utility::extractFields($this->getOriginalObject(), $fields_by_property_path);
           }
-          catch (SearchApiException $e) {
-            // If we couldn't load the object, just log an error and fail
-            // silently to set the values.
-            watchdog_exception('search_api', $e);
+          if ($processors_with_fields) {
+            $processors = $this->index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
+            foreach ($processors as $processor_id => $processor) {
+              if (isset($processors_with_fields[$processor_id])) {
+                $processor->addFieldValues($this);
+              }
+            }
           }
         }
+        catch (SearchApiException $e) {
+          // If we couldn't load the object, just log an error and fail
+          // silently to set the values.
+          watchdog_exception('search_api', $e);
+        }
       }
       $this->fieldsExtracted = TRUE;
     }
diff --git a/src/Plugin/search_api/processor/AddURL.php b/src/Plugin/search_api/processor/AddURL.php
index 022929d..fe42a9a 100644
--- a/src/Plugin/search_api/processor/AddURL.php
+++ b/src/Plugin/search_api/processor/AddURL.php
@@ -2,9 +2,10 @@
 
 namespace Drupal\search_api\Plugin\search_api\processor;
 
-use Drupal\Core\TypedData\DataDefinition;
 use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Processor\ProcessorPluginBase;
+use Drupal\search_api\Processor\ProcessorProperty;
 
 /**
  * Adds the item's URL to the indexed data.
@@ -14,10 +15,10 @@
  *   label = @Translation("URL field"),
  *   description = @Translation("Adds the item's URL to the indexed data."),
  *   stages = {
- *     "preprocess_index" = -30
+ *     "add_properties" = 0,
  *   },
  *   locked = true,
- *   hidden = true
+ *   hidden = true,
  * )
  */
 class AddURL extends ProcessorPluginBase {
@@ -25,29 +26,30 @@ class AddURL extends ProcessorPluginBase {
   /**
    * {@inheritdoc}
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {
-    if ($datasource) {
-      return;
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = array();
+
+    if (!$datasource) {
+      $definition = array(
+        'label' => $this->t('URI'),
+        'description' => $this->t('A URI where the item can be accessed'),
+        'type' => 'string',
+        'processor_id' => $this->getPluginId(),
+      );
+      $properties['search_api_url'] = new ProcessorProperty($definition);
     }
-    $definition = array(
-      'label' => $this->t('URI'),
-      'description' => $this->t('A URI where the item can be accessed'),
-      'type' => 'string',
-    );
-    $properties['search_api_url'] = new DataDefinition($definition);
+
+    return $properties;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function preprocessIndexItems(array &$items) {
-    // Annoyingly, this doc comment is needed for PHPStorm. See
-    // http://youtrack.jetbrains.com/issue/WI-23586
-    /** @var \Drupal\search_api\Item\ItemInterface $item */
-    foreach ($items as $item) {
-      $url = $item->getDatasource()->getItemUrl($item->getOriginalObject());
-      if ($url) {
-        foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_url') as $field) {
+  public function addFieldValues(ItemInterface $item) {
+    $url = $item->getDatasource()->getItemUrl($item->getOriginalObject());
+    if ($url) {
+      foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_url') as $field) {
+        if (!$field->getDatasourceId()) {
           $field->addValue($url->toString());
         }
       }
diff --git a/src/Plugin/search_api/processor/AggregatedFields.php b/src/Plugin/search_api/processor/AggregatedFields.php
index 9fc70a3..036b91c 100644
--- a/src/Plugin/search_api/processor/AggregatedFields.php
+++ b/src/Plugin/search_api/processor/AggregatedFields.php
@@ -3,9 +3,12 @@
 namespace Drupal\search_api\Plugin\search_api\processor;
 
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\TypedData\DataDefinition;
 use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Processor\ProcessorInterface;
 use Drupal\search_api\Processor\ProcessorPluginBase;
+use Drupal\search_api\Processor\ProcessorProperty;
+use Drupal\search_api\Processor\ProcessorPropertyInterface;
 use Drupal\search_api\Utility;
 
 /**
@@ -16,9 +19,10 @@
  *   label = @Translation("Aggregated fields"),
  *   description = @Translation("Add customized aggregations of existing fields to the index."),
  *   stages = {
- *     "pre_index_save" = -10,
- *     "preprocess_index" = -25
- *   }
+ *     "add_properties" = 20,
+ *     "pre_index_save" = 20,
+ *     "preprocess_index" = -25,
+ *   },
  * )
  */
 class AggregatedFields extends ProcessorPluginBase {
@@ -269,23 +273,18 @@ protected function getDatasourceLabelPrefixes() {
    * datasource ID and the property path. This is used internally in this class
    * to easily identify any property on the index.
    *
-   * @param bool $alter
-   *   (optional) Whether to pass the property definitions to the index's
-   *   enabled processors for altering before returning them. Must be set to
-   *   FALSE when called from within alterProperties(), for obvious reasons.
-   *
    * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
    *   All the properties available on the index, keyed by combined ID.
    *
    * @see \Drupal\search_api\Utility::createCombinedId()
    */
-  protected function getAvailableProperties($alter = TRUE) {
+  protected function getAvailableProperties() {
     $properties = array();
 
     $datasource_ids = $this->index->getDatasourceIds();
     $datasource_ids[] = NULL;
     foreach ($datasource_ids as $datasource_id) {
-      foreach ($this->index->getPropertyDefinitions($datasource_id, $alter) as $property_path => $property) {
+      foreach ($this->index->getPropertyDefinitions($datasource_id) as $property_path => $property) {
         $properties[Utility::createCombinedId($datasource_id, $property_path)] = $property;
       }
     }
@@ -357,8 +356,8 @@ public function preIndexSave() {
   /**
    * {@inheritdoc}
    */
-  public function preprocessIndexItems(array &$items) {
-    if (!$items || empty($this->configuration['fields'])) {
+  public function addFieldValues(ItemInterface $item) {
+    if (empty($this->configuration['fields'])) {
       return;
     }
 
@@ -370,8 +369,10 @@ public function preprocessIndexItems(array &$items) {
       return;
     }
 
-    $required_properties_by_datasource = array_fill_keys($this->index->getDatasourceIds(), array());
-    $required_properties_by_datasource[NULL] = array();
+    $required_properties_by_datasource = array(
+      NULL => array(),
+      $item->getDatasourceId() => array(),
+    );
     foreach ($aggregated_fields as $field_definition) {
       foreach ($field_definition['fields'] as $combined_id) {
         list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id);
@@ -379,91 +380,111 @@ public function preprocessIndexItems(array &$items) {
       }
     }
 
-    /** @var \Drupal\search_api\Item\ItemInterface[] $items */
-    foreach ($items as $item) {
-      // Extract the required properties.
-      $property_values = array();
-      /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
-      $missing_fields = array();
-      foreach (array(NULL, $item->getDatasourceId()) as $datasource_id) {
-        foreach ($required_properties_by_datasource[$datasource_id] as $property_path => $combined_id) {
-          // If a field with the right property path is already set on the item,
-          // use it. This might actually make problems in case the values have
-          // already been processed in some way, or use a data type that
-          // transformed their original value – but on the other hand, it's
-          // (currently – see #2575003) the only way to include computed
-          // (processor-added) properties here, so it seems like a fair
-          // trade-off.
-          foreach ($this->filterForPropertyPath($item->getFields(FALSE), $property_path) as $field) {
-            if ($field->getDatasourceId() === $datasource_id) {
-              $property_values[$combined_id] = $field->getValues();
-              continue 2;
-            }
+    // Extract the required properties.
+    $property_values = array();
+    /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
+    $missing_fields = array();
+    $processor_fields = array();
+    $needed_processors = array();
+    foreach (array(NULL, $item->getDatasourceId()) as $datasource_id) {
+      $properties = $this->index->getPropertyDefinitions($datasource_id);
+      foreach ($required_properties_by_datasource[$datasource_id] as $property_path => $combined_id) {
+        // If a field with the right property path is already set on the item,
+        // use it. This might actually make problems in case the values have
+        // already been processed in some way, or use a data type that
+        // transformed their original value. But that will hopefully not be a
+        // problem in most situations.
+        foreach ($this->filterForPropertyPath($item->getFields(FALSE), $property_path) as $field) {
+          if ($field->getDatasourceId() === $datasource_id) {
+            $property_values[$combined_id] = $field->getValues();
+            continue 2;
           }
+        }
 
-          // If the field is not already on the item, we need to extract it. We
-          // set our own combined ID as the field identifier as kind of a hack,
-          // to easily be able to add the field values to $property_values
-          // afterwards.
-          if ($datasource_id) {
-            $missing_fields[$property_path][] = Utility::createField($this->index, $combined_id);
-          }
-          else {
-            // Extracting properties without a datasource is pointless.
-            $property_values[$combined_id] = array();
-          }
+        if (!isset($properties[$property_path])) {
+          continue;
+        }
+
+        // If the field is not already on the item, we need to extract it. We
+        // set our own combined ID as the field identifier as kind of a hack,
+        // to easily be able to add the field values to $property_values
+        // afterwards.
+        $property = $properties[$property_path];
+        if ($property instanceof ProcessorPropertyInterface) {
+          $processor_fields[] = Utility::createField($this->index, $combined_id);
+          $needed_processors[$property->getProcessorId()] = TRUE;
+        }
+        elseif ($datasource_id) {
+          $missing_fields[$property_path][] = Utility::createField($this->index, $combined_id);
+        }
+        else {
+          // Extracting properties without a datasource is pointless.
+          $property_values[$combined_id] = array();
         }
       }
-      if ($missing_fields) {
-        Utility::extractFields($item->getOriginalObject(), $missing_fields);
-        foreach ($missing_fields as $property_fields) {
-          foreach ($property_fields as $field) {
-            $property_values[$field->getFieldIdentifier()] = $field->getValues();
-          }
+    }
+    if ($missing_fields) {
+      Utility::extractFields($item->getOriginalObject(), $missing_fields);
+      foreach ($missing_fields as $property_fields) {
+        foreach ($property_fields as $field) {
+          $property_values[$field->getFieldIdentifier()] = $field->getValues();
+        }
+      }
+    }
+    if ($processor_fields) {
+      $dummy_item = clone $item;
+      $dummy_item->setFields($processor_fields);
+      $processors = $this->index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
+      foreach ($processors as $processor_id => $processor) {
+        if (isset($needed_processors[$processor_id])) {
+          $processor->addFieldValues($dummy_item);
         }
       }
+      foreach ($processor_fields as $field) {
+        $property_values[$field->getFieldIdentifier()] = $field->getValues();
+      }
+    }
 
-      foreach ($this->configuration['fields'] as $aggregated_field_id => $aggregated_field) {
-        $values = array();
-        foreach ($aggregated_field['fields'] as $combined_id) {
-          if (!empty($property_values[$combined_id])) {
-            $values = array_merge($values, $property_values[$combined_id]);
-          }
+    foreach ($this->configuration['fields'] as $aggregated_field_id => $aggregated_field) {
+      $values = array();
+      foreach ($aggregated_field['fields'] as $combined_id) {
+        if (!empty($property_values[$combined_id])) {
+          $values = array_merge($values, $property_values[$combined_id]);
         }
+      }
 
-        switch ($aggregated_field['type']) {
-          case 'concat':
-            $values = array(implode("\n\n", $values));
-            break;
+      switch ($aggregated_field['type']) {
+        case 'concat':
+          $values = array(implode("\n\n", $values));
+          break;
 
-          case 'sum':
-            $values = array(array_sum($values));
-            break;
+        case 'sum':
+          $values = array(array_sum($values));
+          break;
 
-          case 'count':
-            $values = array(count($values));
-            break;
+        case 'count':
+          $values = array(count($values));
+          break;
 
-          case 'max':
-            $values = array(max($values));
-            break;
+        case 'max':
+          $values = array(max($values));
+          break;
 
-          case 'min':
-            $values = array(min($values));
-            break;
+        case 'min':
+          $values = array(min($values));
+          break;
 
-          case 'first':
-            if ($values) {
-              $values = array(reset($values));
-            }
-            break;
+        case 'first':
+          if ($values) {
+            $values = array(reset($values));
+          }
+          break;
 
-        }
+      }
 
-        if ($values) {
-          foreach ($this->filterForPropertyPath($item->getFields(), $aggregated_field_id) as $field) {
-            $field->setValues($values);
-          }
+      if ($values) {
+        foreach ($this->filterForPropertyPath($item->getFields(), $aggregated_field_id) as $field) {
+          $field->setValues($values);
         }
       }
     }
@@ -472,25 +493,26 @@ public function preprocessIndexItems(array &$items) {
   /**
    * {@inheritdoc}
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {
-    if ($datasource) {
-      return;
-    }
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = array();
 
-    $types = $this->getTypes('type');
-    if (!empty($this->configuration['fields'])) {
+    if (!$datasource && !empty($this->configuration['fields'])) {
       // Collect all available properties, keyed by combined ID.
-      $available_properties = $this->getAvailableProperties(FALSE);
+      $types = $this->getTypes('type');
+      $available_properties = $this->getAvailableProperties();
       $datasource_label_prefixes = $this->getDatasourceLabelPrefixes();
       foreach ($this->configuration['fields'] as $aggregated_field_id => $field_definition) {
         $definition = array(
           'label' => $field_definition['label'],
           'description' => $this->fieldDescription($field_definition, $available_properties, $datasource_label_prefixes),
           'type' => $types[$field_definition['type']],
+          'processor_id' => $this->getPluginId(),
         );
-        $properties[$aggregated_field_id] = new DataDefinition($definition);
+        $properties[$aggregated_field_id] = new ProcessorProperty($definition);
       }
     }
+
+    return $properties;
   }
 
   /**
diff --git a/src/Plugin/search_api/processor/ContentAccess.php b/src/Plugin/search_api/processor/ContentAccess.php
index bae30b5..c4bd29e 100644
--- a/src/Plugin/search_api/processor/ContentAccess.php
+++ b/src/Plugin/search_api/processor/ContentAccess.php
@@ -8,9 +8,10 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Session\AnonymousUserSession;
 use Drupal\Core\TypedData\ComplexDataInterface;
-use Drupal\Core\TypedData\DataDefinition;
 use Drupal\node\NodeInterface;
 use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Processor\ProcessorProperty;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\Processor\ProcessorPluginBase;
@@ -26,10 +27,11 @@
  *   label = @Translation("Content access"),
  *   description = @Translation("Adds content access checks for nodes and comments."),
  *   stages = {
+ *     "add_properties" = 0,
  *     "pre_index_save" = -10,
  *     "preprocess_index" = -30,
- *     "preprocess_query" = -30
- *   }
+ *     "preprocess_query" = -30,
+ *   },
  * )
  */
 class ContentAccess extends ProcessorPluginBase {
@@ -90,38 +92,27 @@ public static function supportsIndex(IndexInterface $index) {
   /**
    * {@inheritdoc}
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {
-    $definition = array(
-      'label' => $this->t('Node access information'),
-      'description' => $this->t('Data needed to apply node access.'),
-      'type' => 'string',
-    );
-    $properties['search_api_node_grants'] = new DataDefinition($definition);
-  }
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = array();
 
-  /**
-   * {@inheritdoc}
-   */
-  public function preIndexSave() {
-    foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
-      $entity_type = $datasource->getEntityTypeId();
-      if (in_array($entity_type, array('node', 'comment'))) {
-        $this->ensureField($datasource_id, 'status', 'boolean');
-        if ($entity_type == 'node') {
-          $this->ensureField($datasource_id, 'uid', 'integer');
-        }
-      }
+    if (!$datasource) {
+      $definition = array(
+        'label' => $this->t('Node access information'),
+        'description' => $this->t('Data needed to apply node access.'),
+        'type' => 'string',
+        'processor_id' => $this->getPluginId(),
+        'hidden' => TRUE,
+      );
+      $properties['search_api_node_grants'] = new ProcessorProperty($definition);
     }
 
-    $field = $this->ensureField(NULL, 'search_api_node_grants', 'string');
-    $field->setHidden();
-    $this->index->addField($field);
+    return $properties;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function preprocessIndexItems(array &$items) {
+  public function addFieldValues(ItemInterface $item) {
     static $anonymous_user;
 
     if (!isset($anonymous_user)) {
@@ -129,40 +120,54 @@ public function preprocessIndexItems(array &$items) {
       $anonymous_user = new AnonymousUserSession();
     }
 
-    // Annoyingly, this doc comment is needed for PHPStorm. See
-    // http://youtrack.jetbrains.com/issue/WI-23586
-    /** @var \Drupal\search_api\Item\ItemInterface $item */
-    foreach ($items as $item) {
-      // Only run for node and comment items.
-      if (!in_array($item->getDatasource()->getEntityTypeId(), array('node', 'comment'))) {
-        continue;
-      }
+    // Only run for node and comment items.
+    $entity_type_id = $item->getDatasource()->getEntityTypeId();
+    if (!in_array($entity_type_id, array('node', 'comment'))) {
+      return;
+    }
 
-      // Get the node object.
-      $node = $this->getNode($item->getOriginalObject());
-      if (!$node) {
-        // Apparently we were active for a wrong item.
-        continue;
-      }
+    // Get the node object.
+    $node = $this->getNode($item->getOriginalObject());
+    if (!$node) {
+      // Apparently we were active for a wrong item.
+      return;
+    }
 
-      foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_node_grants') as $field) {
-        // Collect grant information for the node.
-        if (!$node->access('view', $anonymous_user)) {
-          // If anonymous user has no permission we collect all grants with
-          // their realms in the item.
-          $result = Database::getConnection()
-            ->query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->id()));
-          foreach ($result as $grant) {
-            $field->addValue("node_access_{$grant->realm}:{$grant->gid}");
-          }
+    foreach ($this->filterForPropertyPath($item->getFields(), 'search_api_node_grants') as $field) {
+      // Collect grant information for the node.
+      if (!$node->access('view', $anonymous_user)) {
+        // If anonymous user has no permission we collect all grants with
+        // their realms in the item.
+        $result = Database::getConnection()
+          ->query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $node->id()));
+        foreach ($result as $grant) {
+          $field->addValue("node_access_{$grant->realm}:{$grant->gid}");
         }
-        else {
-          // Add the generic pseudo view grant if we are not using node access
-          // or the node is viewable by anonymous users.
-          $field->addValue('node_access__all');
+      }
+      else {
+        // Add the generic pseudo view grant if we are not using node access
+        // or the node is viewable by anonymous users.
+        $field->addValue('node_access__all');
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preIndexSave() {
+    foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+      $entity_type = $datasource->getEntityTypeId();
+      if (in_array($entity_type, array('node', 'comment'))) {
+        $this->ensureField($datasource_id, 'status', 'boolean');
+        if ($entity_type == 'node') {
+          $this->ensureField($datasource_id, 'uid', 'integer');
         }
       }
     }
+
+    $field = $this->ensureField(NULL, 'search_api_node_grants', 'string');
+    $field->setHidden();
   }
 
   /**
@@ -197,7 +202,7 @@ public function preprocessSearchQuery(QueryInterface $query) {
       if (is_numeric($account)) {
         $account = User::load($account);
       }
-      if (is_object($account)) {
+      if ($account instanceof AccountInterface) {
         try {
           $this->addNodeAccess($query, $account);
         }
diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php
index 21427cd..e1bcf7b 100644
--- a/src/Plugin/search_api/processor/RenderedItem.php
+++ b/src/Plugin/search_api/processor/RenderedItem.php
@@ -8,9 +8,10 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Session\AccountProxyInterface;
 use Drupal\Core\Session\UserSession;
-use Drupal\Core\TypedData\DataDefinition;
 use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Processor\ProcessorPluginBase;
+use Drupal\search_api\Processor\ProcessorProperty;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -22,9 +23,10 @@
  *   label = @Translation("Rendered item"),
  *   description = @Translation("Adds an additional field containing the rendered item as it would look when viewed."),
  *   stages = {
+ *     "add_properties" = 0,
  *     "pre_index_save" = -10,
- *     "preprocess_index" = -30
- *   }
+ *     "preprocess_index" = -30,
+ *   },
  * )
  */
 class RenderedItem extends ProcessorPluginBase {
@@ -214,29 +216,26 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
   /**
    * {@inheritdoc}
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {
-    if ($datasource) {
-      return;
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    $properties = array();
+
+    if (!$datasource) {
+      $definition = array(
+        'type' => 'text',
+        'label' => $this->t('Rendered HTML output'),
+        'description' => $this->t('The complete HTML which would be displayed when viewing the item'),
+        'processor_id' => $this->getPluginId(),
+      );
+      $properties['rendered_item'] = new ProcessorProperty($definition);
     }
-    $definition = array(
-      'type' => 'text',
-      'label' => $this->t('Rendered HTML output'),
-      'description' => $this->t('The complete HTML which would be displayed when viewing the item'),
-    );
-    $properties['rendered_item'] = new DataDefinition($definition);
-  }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function preIndexSave() {
-    $this->ensureField(NULL, 'rendered_item');
+    return $properties;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function preprocessIndexItems(array &$items) {
+  public function addFieldValues(ItemInterface $item) {
     // Change the current user to our dummy implementation to ensure we are
     // using the configured roles.
     $original_user = $this->currentUser->getAccount();
@@ -245,35 +244,33 @@ public function preprocessIndexItems(array &$items) {
     // Count of items that don't have a view mode.
     $unset_view_modes = 0;
 
-    // Annoyingly, this doc comment is needed for PHPStorm. See
-    // http://youtrack.jetbrains.com/issue/WI-23586
-    /** @var \Drupal\search_api\Item\ItemInterface $item */
-    foreach ($items as $item) {
-      foreach ($this->filterForPropertyPath($item->getFields(), 'rendered_item') as $field) {
-        $datasource_id = $item->getDatasourceId();
-        $datasource = $item->getDatasource();
-        $bundle = $datasource->getItemBundle($item->getOriginalObject());
-        // When no view mode has been set for the bundle, or it has been set to
-        // "Don't include the rendered item", skip this item.
-        if (empty($this->configuration['view_mode'][$datasource_id][$bundle])) {
-          // If it was really not set, also notify the user through the log.
-          if (!isset($this->configuration['view_mode'][$datasource_id][$bundle])) {
-            ++$unset_view_modes;
-          }
-          continue;
-        }
-        else {
-          $view_mode = (string) $this->configuration['view_mode'][$datasource_id][$bundle];
+    foreach ($this->filterForPropertyPath($item->getFields(), 'rendered_item') as $field) {
+      $datasource_id = $item->getDatasourceId();
+      $datasource = $item->getDatasource();
+      $bundle = $datasource->getItemBundle($item->getOriginalObject());
+      // When no view mode has been set for the bundle, or it has been set to
+      // "Don't include the rendered item", skip this item.
+      if (empty($this->configuration['view_mode'][$datasource_id][$bundle])) {
+        // If it was really not set, also notify the user through the log.
+        if (!isset($this->configuration['view_mode'][$datasource_id][$bundle])) {
+          ++$unset_view_modes;
         }
+        continue;
+      }
+      else {
+        $view_mode = (string) $this->configuration['view_mode'][$datasource_id][$bundle];
+      }
 
-        $build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
-        $value = (string) $this->getRenderer()->renderPlain($build);
-        if ($value) {
-          $field->addValue($value);
-        }
+      $build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
+      $value = (string) $this->getRenderer()->renderPlain($build);
+      if ($value) {
+        $field->addValue($value);
       }
     }
 
+    // Restore the original user.
+    $this->currentUser->setAccount($original_user);
+
     if ($unset_view_modes > 0) {
       $context = array(
         '%index' => $this->index->label(),
@@ -282,9 +279,13 @@ public function preprocessIndexItems(array &$items) {
       );
       $this->getLogger()->warning('Warning: While indexing items on search index %index, @count item(s) did not have a view mode configured for the %processor processor.', $context);
     }
+  }
 
-    // Restore the original user.
-    $this->currentUser->setAccount($original_user);
+  /**
+   * {@inheritdoc}
+   */
+  public function preIndexSave() {
+    $this->ensureField(NULL, 'rendered_item');
   }
 
   /**
diff --git a/src/Processor/ProcessorInterface.php b/src/Processor/ProcessorInterface.php
index 4c28e27..89d2029 100644
--- a/src/Processor/ProcessorInterface.php
+++ b/src/Processor/ProcessorInterface.php
@@ -4,6 +4,7 @@
 
 use Drupal\search_api\Datasource\DatasourceInterface;
 use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Plugin\IndexPluginInterface;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSetInterface;
@@ -25,6 +26,11 @@
 interface ProcessorInterface extends IndexPluginInterface {
 
   /**
+   * Processing stage: add properties.
+   */
+  const STAGE_ADD_PROPERTIES = 'add_properties';
+
+  /**
    * Processing stage: preprocess index.
    */
   const STAGE_PRE_INDEX_SAVE = 'pre_index_save';
@@ -108,15 +114,24 @@ public function isLocked();
   public function isHidden();
 
   /**
-   * Alters the given datasource's property definitions.
+   * Retrieves the properties this processor defines for the given datasource.
    *
-   * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
-   *   An array of property definitions for this datasource.
    * @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
    *   (optional) The datasource this set of properties belongs to. If NULL, the
    *   datasource-independent properties should be added (or modified).
+   *
+   * @return \Drupal\search_api\Processor\ProcessorPropertyInterface[]
+   *   An array of property definitions for that datasource.
+   */
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL);
+
+  /**
+   * Adds the values of properties defined by this processor to the item.
+   *
+   * @param \Drupal\search_api\Item\ItemInterface $item
+   *   The item whose field values should be added.
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL);
+  public function addFieldValues(ItemInterface $item);
 
   /**
    * Preprocesses the search index entity before it is saved.
diff --git a/src/Processor/ProcessorPluginBase.php b/src/Processor/ProcessorPluginBase.php
index 0ee4393..9bb5c20 100644
--- a/src/Processor/ProcessorPluginBase.php
+++ b/src/Processor/ProcessorPluginBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\search_api\Datasource\DatasourceInterface;
 use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Item\ItemInterface;
 use Drupal\search_api\Plugin\IndexPluginBase;
 use Drupal\search_api\Query\QueryInterface;
 use Drupal\search_api\Query\ResultSetInterface;
@@ -83,7 +84,14 @@ public function isHidden() {
   /**
    * {@inheritdoc}
    */
-  public function alterPropertyDefinitions(array &$properties, DatasourceInterface $datasource = NULL) {}
+  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addFieldValues(ItemInterface $item) {}
 
   /**
    * {@inheritdoc}
@@ -122,13 +130,13 @@ protected function ensureField($datasource_id, $property_path, $type = NULL) {
         throw new SearchApiException("Could not find property '$property_id' which is required by the '$processor_label' processor.");
       }
       $field = Utility::createFieldFromProperty($this->index, $property, $datasource_id, $property_path, NULL, $type);
+      $this->index->addField($field);
     }
 
     $field->setIndexedLocked();
     if (isset($type)) {
       $field->setTypeLocked();
     }
-    $this->index->addField($field);
     return $field;
   }
 
diff --git a/src/Processor/ProcessorProperty.php b/src/Processor/ProcessorProperty.php
new file mode 100644
index 0000000..e847717
--- /dev/null
+++ b/src/Processor/ProcessorProperty.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\search_api\Processor;
+
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Provides a base class for normal processor-defined properties.
+ */
+class ProcessorProperty extends DataDefinition implements ProcessorPropertyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProcessorId() {
+    return $this->definition['processor_id'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isHidden() {
+    return !empty($this->definition['hidden']);
+  }
+
+}
diff --git a/src/Processor/ProcessorPropertyInterface.php b/src/Processor/ProcessorPropertyInterface.php
new file mode 100644
index 0000000..3b5148f
--- /dev/null
+++ b/src/Processor/ProcessorPropertyInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\search_api\Processor;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+
+/**
+ * Provides an interface for processor-defined properties.
+ */
+interface ProcessorPropertyInterface extends DataDefinitionInterface {
+
+  /**
+   * Retrieves the ID of the processor which defines this property.
+   *
+   * @return string
+   *   The defining processor's plugin ID.
+   */
+  public function getProcessorId();
+
+  /**
+   * Determines whether this property should be hidden from the UI.
+   *
+   * @return bool
+   *   TRUE if this property should not be displayed in the UI, FALSE otherwise.
+   */
+  public function isHidden();
+
+}
diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php
index 3725fa2..935633c 100644
--- a/src/Tests/IntegrationTest.php
+++ b/src/Tests/IntegrationTest.php
@@ -3,6 +3,10 @@
 namespace Drupal\search_api\Tests;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\search_api\Entity\Server;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\Utility;
@@ -588,18 +592,19 @@ protected function checkFieldLabels() {
     $this->assertResponse(200);
     $this->drupalPostForm(NULL, $edit, $this->t('Save and manage fields'));
 
-    $field_name = '^6%{[*>.<"field';
-
     // Add a field to that content type with funky chars.
-    $edit = array(
-      'new_storage_type' => 'string',
+    $field_name = '^6%{[*>.<"field';
+    FieldStorageConfig::create(array(
+      'field_name' => 'field__field_',
+      'type' => 'string',
+      'entity_type' => 'node',
+    ))->save();
+    FieldConfig::create(array(
+      'field_name' => 'field__field_',
+      'entity_type' => 'node',
+      'bundle' => '_content_',
       'label' => $field_name,
-      'field_name' => '_field_',
-    );
-    $this->drupalGet('admin/structure/types/manage/_content_/fields/add-field');
-    $this->assertResponse(200);
-    $this->drupalPostForm(NULL, $edit, $this->t('Save and continue'));
-    $this->drupalPostForm(NULL, array(), $this->t('Save field settings'));
+    ))->save();
 
     $url_options['query']['datasource'] = 'entity:node';
     $this->drupalGet($this->getIndexPath('fields/add'), $url_options);
@@ -744,33 +749,37 @@ protected function addField($datasource_id, $property_path, $label = NULL) {
    * Tests field dependencies.
    */
   protected function addFieldsWithDependenciesToIndex() {
-    // Add a new field.
-    $edit = array(
-      'new_storage_type' => 'link',
+    // Add a new link field.
+    FieldStorageConfig::create(array(
+      'field_name' => 'field_link',
+      'type' => 'link',
+      'entity_type' => 'node',
+    ))->save();
+    FieldConfig::create(array(
+      'field_name' => 'field_link',
+      'entity_type' => 'node',
+      'bundle' => 'article',
       'label' => 'Link',
-      'field_name' => 'link',
-    );
-    $this->drupalPostForm('admin/structure/types/manage/article/fields/add-field', $edit, t('Save and continue'));
-    $this->drupalPostForm(NULL, array(), t('Save field settings'));
-    $this->drupalPostForm(NULL, array(), t('Save settings'));
-
-    // Add an image field.
-    $edit = array(
-      'new_storage_type' => 'image',
+    ))->save();
+
+    // Add a new image field, for both articles and basic pages.
+    FieldStorageConfig::create(array(
+      'field_name' => 'field_image',
+      'type' => 'image',
+      'entity_type' => 'node',
+    ))->save();
+    FieldConfig::create(array(
+      'field_name' => 'field_image',
+      'entity_type' => 'node',
+      'bundle' => 'article',
       'label' => 'Image',
-      'field_name' => 'image',
-    );
-    $this->drupalPostForm('admin/structure/types/manage/article/fields/add-field', $edit, t('Save and continue'));
-    $this->drupalPostForm(NULL, array(), t('Save field settings'));
-    $this->drupalPostForm(NULL, array(), t('Save settings'));
-
-    // Add the image field to the "Basic page" content type, too.
-    $edit = array(
-      'existing_storage_name' => 'field_image',
-      'existing_storage_label' => 'Image',
-    );
-    $this->drupalPostForm('admin/structure/types/manage/page/fields/add-field', $edit, t('Save and continue'));
-    $this->drupalPostForm(NULL, array(), t('Save settings'));
+    ))->save();
+    FieldConfig::create(array(
+      'field_name' => 'field_image',
+      'entity_type' => 'node',
+      'bundle' => 'page',
+      'label' => 'Image',
+    ))->save();
 
     $fields = array(
       'field_link' => $this->t('Link'),
@@ -838,15 +847,32 @@ protected function removeFieldsFromIndex() {
    * Tests if non-base fields of referenced entities can be added.
    */
   protected function checkReferenceFieldsNonBaseFields() {
-    // Add a entity_reference field.
+    // Add a new entity_reference field.
     $field_label = 'reference_field';
-    $edit = array(
-      'new_storage_type' => 'entity_reference',
+    FieldStorageConfig::create(array(
+      'field_name' => 'field__reference_field_',
+      'type' => 'entity_reference',
+      'entity_type' => 'node',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      'settings' => array(
+        'allowed_values' => array(
+          array(
+            'target_type' => 'node',
+          ),
+        ),
+      ),
+    ))->save();
+    FieldConfig::create(array(
+      'field_name' => 'field__reference_field_',
+      'entity_type' => 'node',
+      'bundle' => 'article',
       'label' => $field_label,
-      'field_name' => '_reference_field_',
-    );
-    $this->drupalPostForm('admin/structure/types/manage/article/fields/add-field', $edit, $this->t('Save and continue'));
-    $this->drupalPostForm(NULL, array('cardinality' => -1), $this->t('Save field settings'));
+    ))->save();
+    EntityFormDisplay::load('node.article.default')
+      ->setComponent('field__reference_field_', array(
+        'type' => 'entity_reference_autocomplete',
+      ))
+      ->save();
 
     $node_label = $this->getIndex()->getDatasource('entity:node')->label();
     $field_label = "$field_label » $node_label » $field_label";
diff --git a/src/UnsavedIndexConfiguration.php b/src/UnsavedIndexConfiguration.php
index 7b21c9e..c186196 100644
--- a/src/UnsavedIndexConfiguration.php
+++ b/src/UnsavedIndexConfiguration.php
@@ -350,8 +350,8 @@ public function getFulltextFields() {
   /**
    * {@inheritdoc}
    */
-  public function getPropertyDefinitions($datasource_id, $alter = TRUE) {
-    return $this->entity->getPropertyDefinitions($datasource_id, $alter);
+  public function getPropertyDefinitions($datasource_id) {
+    return $this->entity->getPropertyDefinitions($datasource_id);
   }
 
   /**
diff --git a/tests/src/Kernel/CustomDataTypesTest.php b/tests/src/Kernel/CustomDataTypesTest.php
index 66d5563..31360e1 100644
--- a/tests/src/Kernel/CustomDataTypesTest.php
+++ b/tests/src/Kernel/CustomDataTypesTest.php
@@ -106,8 +106,14 @@ public function setUp() {
     ));
     $this->server->save();
 
+    // Set the server (determines the supported data types) and remove all
+    // non-base fields from the index (since their config isn't installed).
     $this->index = Index::load('database_search_index');
-    $this->index->setServer($this->server);
+    $this->index->setServer($this->server)
+      ->removeField('body')
+      ->removeField('keywords')
+      ->removeField('category')
+      ->removeField('width');
   }
 
   /**
diff --git a/tests/src/Kernel/Processor/ContentAccessTest.php b/tests/src/Kernel/Processor/ContentAccessTest.php
index ab230f7..1964845 100644
--- a/tests/src/Kernel/Processor/ContentAccessTest.php
+++ b/tests/src/Kernel/Processor/ContentAccessTest.php
@@ -6,6 +6,7 @@
 use Drupal\comment\Entity\CommentType;
 use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\Core\Database\Database;
+use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\search_api\Query\ResultSetInterface;
@@ -239,7 +240,10 @@ public function testContentAccessAll() {
     }
     $items = $this->generateItems($items);
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     foreach ($items as $item) {
       $this->assertEquals(array('node_access__all'), $item->getField('search_api_node_grants')->getValues());
@@ -261,7 +265,10 @@ public function testContentAccessWithNodeGrants() {
     }
     $items = $this->generateItems($items);
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     foreach ($items as $item) {
       $this->assertEquals(array('node_access_search_api_test:0'), $item->getField('search_api_node_grants')->getValues());
@@ -292,6 +299,21 @@ public function testNodeGrantsChange() {
   }
 
   /**
+   * Tests whether the property is correctly added by the processor.
+   */
+  public function testAlterPropertyDefinitions() {
+    // Check for added properties when no datasource is given.
+    $properties = $this->processor->getPropertyDefinitions(NULL);
+    $this->assertTrue(array_key_exists('search_api_node_grants', $properties), 'The Properties where modified with the "search_api_node_grants".');
+    $this->assertTrue(($properties['search_api_node_grants'] instanceof DataDefinitionInterface), 'The "search_api_node_grants" key contains a valid DataDefinition instance.');
+    $this->assertEquals('string', $properties['search_api_node_grants']->getDataType(), 'Correct DataType set in the DataDefinition.');
+
+    // Verify that there are no properties if a datasource is given.
+    $properties = $this->processor->getPropertyDefinitions($this->index->getDatasource('entity:node'));
+    $this->assertEquals(array(), $properties, '"search_api_node_grants" property not added when data source is given.');
+  }
+
+  /**
    * Asserts that the search results contain the expected IDs.
    *
    * @param \Drupal\search_api\Query\ResultSetInterface $result
@@ -312,7 +334,9 @@ protected function assertResults(ResultSetInterface $result, array $expected) {
           $id = $i . ':en';
         }
         else {
-          $id = $this->{"{$entity_type}s"}[$i]->id() . ':en';
+          /** @var \Drupal\Core\Entity\EntityInterface $entity */
+          $entity = $this->{"{$entity_type}s"}[$i];
+          $id = $entity->id() . ':en';
         }
         $ids[] = Utility::createCombinedId($datasource_id, $id);
       }
diff --git a/tests/src/Kernel/Processor/RenderedItemTest.php b/tests/src/Kernel/Processor/RenderedItemTest.php
index 91ba984..e214ba9 100644
--- a/tests/src/Kernel/Processor/RenderedItemTest.php
+++ b/tests/src/Kernel/Processor/RenderedItemTest.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\search_api\Kernel\Processor;
 
-use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\search_api\Utility;
@@ -152,7 +152,7 @@ public function testAddProcessor() {
   /**
    * Tests whether the rendered_item field is correctly filled by the processor.
    */
-  public function testPreprocessIndexItems() {
+  public function testAddFieldValues() {
     $items = array();
     foreach ($this->nodes as $node) {
       $items[] = array(
@@ -164,7 +164,11 @@ public function testPreprocessIndexItems() {
     }
     $items = $this->generateItems($items);
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
+
     foreach ($items as $key => $item) {
       list(, $nid) = Utility::splitCombinedId($key);
       $field = $item->getField('rendered_item');
@@ -213,8 +217,10 @@ public function testHideRenderedItem() {
     }
     $items = $this->generateItems($items);
 
-    // Preprocess the items for indexing.
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     // Verify that no field values were added.
     foreach ($items as $key => $item) {
@@ -227,17 +233,14 @@ public function testHideRenderedItem() {
    * Tests whether the property is correctly added by the processor.
    */
   public function testAlterPropertyDefinitions() {
-    // Check for modified properties when no datasource is given.
-    /** @var \Drupal\Core\TypedData\DataDefinitionInterface[] $properties */
-    $properties = array();
-    $this->processor->alterPropertyDefinitions($properties, NULL);
+    // Check for added properties when no datasource is given.
+    $properties = $this->processor->getPropertyDefinitions(NULL);
     $this->assertTrue(array_key_exists('rendered_item', $properties), 'The Properties where modified with the "rendered_item".');
-    $this->assertTrue(($properties['rendered_item'] instanceof DataDefinition), 'The "rendered_item" contains a valid DataDefinition instance.');
+    $this->assertTrue(($properties['rendered_item'] instanceof DataDefinitionInterface), 'The "rendered_item" contains a valid DataDefinition instance.');
     $this->assertEquals('text', $properties['rendered_item']->getDataType(), 'Correct DataType set in the DataDefinition.');
 
-    // Check if the properties stay untouched if a datasource is given.
-    $properties = array();
-    $this->processor->alterPropertyDefinitions($properties, $this->index->getDatasource('entity:node'));
+    // Verify that there are no properties if a datasource is given.
+    $properties = $this->processor->getPropertyDefinitions($this->index->getDatasource('entity:node'));
     $this->assertEquals(array(), $properties, '"render_item" property not added when data source is given.');
   }
 
diff --git a/tests/src/Unit/Plugin/Processor/AddURLTest.php b/tests/src/Unit/Plugin/Processor/AddURLTest.php
index 9f93e46..9edab74 100644
--- a/tests/src/Unit/Plugin/Processor/AddURLTest.php
+++ b/tests/src/Unit/Plugin/Processor/AddURLTest.php
@@ -70,9 +70,9 @@ protected function setUp() {
   }
 
   /**
-   * Tests whether indexed items are correctly preprocessed.
+   * Tests whether the "URI" field is correctly filled by the processor.
    */
-  public function testProcessIndexItems() {
+  public function testAddFieldValues() {
     /** @var \Drupal\node\Entity\Node $node */
     $node = $this->getMockBuilder('Drupal\node\Entity\Node')
       ->disableOriginalConstructor()
@@ -90,8 +90,10 @@ public function testProcessIndexItems() {
     );
     $items = $this->createItems($this->index, 2, $fields, EntityAdapter::createFromEntity($node));
 
-    // Process the items.
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     // Check the valid item.
     $field = $items[$this->itemIds[0]]->getField('search_api_url');
@@ -112,10 +114,8 @@ public function testProcessIndexItems() {
    * @see \Drupal\search_api\Plugin\search_api\processor\AddURL::alterPropertyDefinitions()
    */
   public function testAlterPropertyDefinitions() {
-    $properties = array();
-
-    // Check for modified properties when no data source is given.
-    $this->processor->alterPropertyDefinitions($properties, NULL);
+    // Check for added properties when no datasource is given.
+    $properties = $this->processor->getPropertyDefinitions(NULL);
     $property_added = array_key_exists('search_api_url', $properties);
     $this->assertTrue($property_added, 'The "search_api_url" property was added to the properties.');
     if ($property_added) {
@@ -127,11 +127,9 @@ public function testAlterPropertyDefinitions() {
       }
     }
 
-    // Test whether the properties of specific datasources stay untouched.
-    $properties = array();
-    /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+    // Verify that there are no properties if a datasource is given.
     $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
-    $this->processor->alterPropertyDefinitions($properties, $datasource);
+    $properties = $this->processor->getPropertyDefinitions($datasource);
     $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
   }
 
diff --git a/tests/src/Unit/Plugin/Processor/AggregatedFieldTest.php b/tests/src/Unit/Plugin/Processor/AggregatedFieldTest.php
index 194eb76..c513de1 100644
--- a/tests/src/Unit/Plugin/Processor/AggregatedFieldTest.php
+++ b/tests/src/Unit/Plugin/Processor/AggregatedFieldTest.php
@@ -84,7 +84,10 @@ public function testUnionAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array('foo', 'bar', 'baz');
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "union" aggregation for item 1.');
@@ -132,7 +135,10 @@ public function testConcatAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array("foo\n\nbar\n\nbaz");
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "concat" aggregation for item 1.');
@@ -180,7 +186,10 @@ public function testSumAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array(22);
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "sum" aggregation for item 1.');
@@ -228,7 +237,10 @@ public function testCountAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array(3);
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "count" aggregation for item 1.');
@@ -276,7 +288,10 @@ public function testMaxAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array(16);
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "max" aggregation for item 1.');
@@ -324,7 +339,10 @@ public function testMinAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array(2);
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "min" aggregation for item 1.');
@@ -372,7 +390,10 @@ public function testFirstAggregation() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $expected = array('foo');
     $this->assertEquals($expected, $items[$this->itemIds[0]]->getField($field_id)->getValues(), 'Correct "first" aggregation for item 1.');
@@ -411,7 +432,10 @@ public function testUnindexedAggregatedField() {
     );
     $items = $this->createItems($this->index, 2, $fields, NULL, array('entity:test1', 'entity:test2'));
 
-    $this->processor->preprocessIndexItems($items);
+    // Add the processor's field values to the items.
+    foreach ($items as $item) {
+      $this->processor->addFieldValues($item);
+    }
 
     $this->assertEquals(NULL, $items[$this->itemIds[0]]->getField('search_api_aggregation_1'), 'Unindexed aggregated field was not added for item 1.');
     $this->assertEquals(NULL, $items[$this->itemIds[1]]->getField('search_api_aggregation_1'), 'Unindexed aggregated field was not added for item 2.');
@@ -474,9 +498,8 @@ public function testAlterPropertyDefinitions() {
     $translation = $this->getStringTranslationStub();
     $this->processor->setStringTranslation($translation);
 
-    // Check for modified properties when no datasource is given.
-    $properties = array();
-    $this->processor->alterPropertyDefinitions($properties, NULL);
+    // Check for added properties when no datasource is given.
+    $properties = $this->processor->getPropertyDefinitions(NULL);
 
     $property_added = array_key_exists('search_api_aggregation_1', $properties);
     $this->assertTrue($property_added, 'The "search_api_aggregation_1" property was added to the properties.');
@@ -503,11 +526,9 @@ public function testAlterPropertyDefinitions() {
       }
     }
 
-    // Test whether the properties of specific datasources stay untouched.
-    $properties = array();
-    /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+    // Verify that there are no properties if a datasource is given.
     $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
-    $this->processor->alterPropertyDefinitions($properties, $datasource);
+    $properties = $this->processor->getPropertyDefinitions($datasource);
     $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
   }
 
