/**
  * Adds a property to be retrieved.
  *
  * Currently doesn't serve any purpose, but might be added to the search query
  * in the future to help backends that support returning fields determine
  * which of the fields should actually be returned.
  *
  * @param string $combined_property_path
  *   The combined property path of the property that should be retrieved.
  *
  * @return $this
  */
 public function addRetrievedProperty($combined_property_path)
 {
     list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
     $this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
     return $this;
 }
 /**
  * Form submission handler for adding a new field to the index.
  *
  * @param array $form
  *   An associative array containing the structure of the form.
  * @param \Drupal\Core\Form\FormStateInterface $form_state
  *   The current state of the form.
  */
 public function addField(array $form, FormStateInterface $form_state)
 {
     $button = $form_state->getTriggeringElement();
     if (!$button) {
         return;
     }
     /** @var \Drupal\Core\TypedData\DataDefinitionInterface $property */
     $property = $button['#property'];
     list($datasource_id, $property_path) = Utility::splitCombinedId($button['#name']);
     $field = Utility::createFieldFromProperty($this->entity, $property, $datasource_id, $property_path, NULL, $button['#data_type']);
     $field->setLabel($button['#prefixed_label']);
     $this->entity->addField($field);
     $args['%label'] = $field->getLabel();
     drupal_set_message($this->t('Field %label was added to the index.', $args));
 }
Beispiel #3
0
 /**
  * {@inheritdoc}
  */
 public function trackItemsInserted(array $ids)
 {
     $transaction = $this->getDatabaseConnection()->startTransaction();
     try {
         $index_id = $this->getIndex()->id();
         // Process the IDs in chunks so we don't create an overly large INSERT
         // statement.
         foreach (array_chunk($ids, 1000) as $ids_chunk) {
             $insert = $this->createInsertStatement();
             foreach ($ids_chunk as $item_id) {
                 list($datasource_id) = Utility::splitCombinedId($item_id);
                 $insert->values(array('index_id' => $index_id, 'datasource' => $datasource_id, 'item_id' => $item_id, 'changed' => REQUEST_TIME, 'status' => $this::STATUS_NOT_INDEXED));
             }
             $insert->execute();
         }
     } catch (\Exception $e) {
         watchdog_exception('search_api', $e);
         $transaction->rollback();
     }
 }
 /**
  * Creates a certain number of test items.
  *
  * @param \Drupal\search_api\IndexInterface $index
  *   The index that should be used for the items.
  * @param int $count
  *   The number of items to create.
  * @param array[] $fields
  *   The fields to create on the items, with keys being field IDs and values
  *   being arrays with the following information:
  *   - type: The type to set for the field.
  *   - values: (optional) The values to set for the field.
  * @param \Drupal\Core\TypedData\ComplexDataInterface|null $object
  *   (optional) The object to set on each item as the "original object".
  * @param array|null $datasource_ids
  *   (optional) An array of datasource IDs to use for the items, in that order
  *   (starting again from the front if necessary). Defaults to using
  *   "entity:node" for all items.
  *
  * @return \Drupal\search_api\Item\ItemInterface[]
  *   An array containing the requested test items.
  */
 public function createItems(IndexInterface $index, $count, array $fields, ComplexDataInterface $object = NULL, array $datasource_ids = NULL)
 {
     if (!isset($datasource_ids)) {
         $datasource_ids = array('entity:node');
     }
     $datasource_count = count($datasource_ids);
     $items = array();
     for ($i = 0; $i < $count; ++$i) {
         $datasource_id = $datasource_ids[$i % $datasource_count];
         $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, $i + 1 . ':en');
         $item = Utility::createItem($index, $item_id);
         if (isset($object)) {
             $item->setOriginalObject($object);
         }
         foreach ($fields as $field_id => $field_info) {
             // Only add fields of the right datasource.
             list($field_datasource_id) = Utility::splitCombinedId($field_id);
             if (isset($field_datasource_id) && $field_datasource_id != $datasource_id) {
                 continue;
             }
             $field = Utility::createField($index, $field_id)->setType($field_info['type']);
             if (isset($field_info['values'])) {
                 $field->setValues($field_info['values']);
             }
             $item->setField($field_id, $field);
         }
         $item->setFieldsExtracted(TRUE);
         $items[$item_id] = $item;
     }
     return $items;
 }
 /**
  * Tests whether the rendered_item field is correctly filled by the processor.
  */
 public function testPreprocessIndexItems()
 {
     $items = array();
     foreach ($this->nodes as $node) {
         $items[] = array('datasource' => 'entity:node', 'item' => $node->getTypedData(), 'item_id' => $node->id(), 'text' => 'node text' . $node->id());
     }
     $items = $this->generateItems($items);
     $this->processor->preprocessIndexItems($items);
     foreach ($items as $key => $item) {
         list(, $nid) = Utility::splitCombinedId($key);
         $field = $item->getField('rendered_item');
         $this->assertEquals('text', $field->getType(), 'Node item ' . $nid . ' rendered value is identified as text.');
         $values = $field->getValues();
         // Test that the value is a string (not, e.g., a SafeString object).
         $this->assertTrue(is_string($values[0]), 'Node item ' . $nid . ' rendered value is a string.');
         $this->assertEquals(1, count($values), 'Node item ' . $nid . ' rendered value is a single value.');
         // These tests rely on the template not changing. However, if we'd only
         // check whether the field values themselves are included, there could
         // easier be false positives. For example the title text was present even
         // when the processor was broken, because the schema metadata was also
         // adding it to the output.
         $this->assertTrue(substr_count($values[0], 'view-mode-full') > 0, 'Node item ' . $nid . ' rendered in view-mode "full".');
         $this->assertTrue(substr_count($values[0], 'field--name-title') > 0, 'Node item ' . $nid . ' has a rendered title field.');
         $this->assertTrue(substr_count($values[0], '>' . $this->nodes[$nid]->label() . '<') > 0, 'Node item ' . $nid . ' has a rendered title inside HTML-Tags.');
         $this->assertTrue(substr_count($values[0], '>Member for<') > 0, 'Node item ' . $nid . ' has rendered member information HTML-Tags.');
         $this->assertTrue(substr_count($values[0], '>' . $this->nodes[$nid]->get('body')->getValue()[0]['value'] . '<') > 0, 'Node item ' . $nid . ' has rendered content inside HTML-Tags.');
     }
 }
Beispiel #6
0
 /**
  * {@inheritdoc}
  */
 public function loadItemsMultiple(array $item_ids, $group_by_datasource = FALSE)
 {
     $items_by_datasource = array();
     foreach ($item_ids as $item_id) {
         list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id);
         $items_by_datasource[$datasource_id][$item_id] = $raw_id;
     }
     $items = array();
     foreach ($items_by_datasource as $datasource_id => $raw_ids) {
         try {
             foreach ($this->getDatasource($datasource_id)->loadMultiple($raw_ids) as $raw_id => $item) {
                 $id = Utility::createCombinedId($datasource_id, $raw_id);
                 if ($group_by_datasource) {
                     $items[$datasource_id][$id] = $item;
                 } else {
                     $items[$id] = $item;
                 }
             }
         } catch (SearchApiException $e) {
             watchdog_exception('search_api', $e);
         }
     }
     return $items;
 }
 /**
  * Creates a description for an aggregated field.
  *
  * @param array $field_definition
  *   The settings of the aggregated field.
  * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
  *   All available properties on the index, keyed by combined ID.
  * @param string[] $datasource_label_prefixes
  *   The label prefixes for all datasources.
  *
  * @return string
  *   A description for the given aggregated field.
  */
 protected function fieldDescription(array $field_definition, array $properties, array $datasource_label_prefixes)
 {
     $fields = array();
     foreach ($field_definition['fields'] as $combined_id) {
         list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id);
         $label = $property_path;
         if (isset($properties[$combined_id])) {
             $label = $properties[$combined_id]->getLabel();
         }
         $fields[] = $datasource_label_prefixes[$datasource_id] . $label;
     }
     $type = $this->getTypes()[$field_definition['type']];
     return $this->t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields)));
 }
 /**
  * Retrieves the property path of the parent property.
  *
  * @return string|null
  *   The property path of the parent property.
  */
 protected function getParentPath()
 {
     if (!isset($this->parentPath)) {
         $combined_property_path = $this->getCombinedPropertyPath();
         list(, $property_path) = Utility::splitCombinedId($combined_property_path);
         list($this->parentPath) = Utility::splitPropertyPath($property_path);
     }
     return $this->parentPath;
 }
Beispiel #9
0
 /**
  * {@inheritdoc}
  */
 public function getDatasourceId()
 {
     if (!isset($this->datasourceId)) {
         list($this->datasourceId) = Utility::splitCombinedId($this->id);
     }
     return $this->datasourceId;
 }
Beispiel #10
0
 /**
  * Creates a certain number of test items.
  *
  * @param \Drupal\search_api\IndexInterface $index
  *   The index that should be used for the items.
  * @param int $count
  *   The number of items to create.
  * @param array[] $fields
  *   The fields to create on the items, with keys being combined property
  *   paths and values being arrays with properties to set on the field.
  * @param \Drupal\Core\TypedData\ComplexDataInterface|null $object
  *   (optional) The object to set on each item as the "original object".
  * @param array|null $datasource_ids
  *   (optional) An array of datasource IDs to use for the items, in that order
  *   (starting again from the front if necessary).
  *
  * @return \Drupal\search_api\Item\ItemInterface[]
  *   An array containing the requested test items.
  */
 public function createItems(IndexInterface $index, $count, array $fields, ComplexDataInterface $object = NULL, array $datasource_ids = array('entity:node'))
 {
     $datasource_count = count($datasource_ids);
     $items = array();
     for ($i = 0; $i < $count; ++$i) {
         $datasource_id = $datasource_ids[$i % $datasource_count];
         $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, $i + 1 . ':en');
         $item = Utility::createItem($index, $item_id);
         if (isset($object)) {
             $item->setOriginalObject($object);
         }
         foreach ($fields as $combined_property_path => $field_info) {
             list($field_info['datasource_id'], $field_info['property_path']) = Utility::splitCombinedId($combined_property_path);
             // Only add fields of the right datasource.
             if (isset($field_info['datasource_id']) && $field_info['datasource_id'] != $datasource_id) {
                 continue;
             }
             $field_id = Utility::getNewFieldId($index, $field_info['property_path']);
             $field = Utility::createField($index, $field_id, $field_info);
             $item->setField($field_id, $field);
         }
         $item->setFieldsExtracted(TRUE);
         $items[$item_id] = $item;
     }
     return $items;
 }
Beispiel #11
0
/**
 * Allows you to log or alter the items that are indexed.
 *
 * Please be aware that generally preventing the indexing of certain items is
 * deprecated. This is better done with processors, which can easily be
 * configured and only added to indexes where this behaviour is wanted.
 * If your module will use this hook to reject certain items from indexing,
 * please document this clearly to avoid confusion.
 *
 * @param \Drupal\search_api\IndexInterface $index
 *   The search index on which items will be indexed.
 * @param \Drupal\search_api\Item\ItemInterface[] $items
 *   The items that will be indexed.
 */
function hook_search_api_index_items_alter(\Drupal\search_api\IndexInterface $index, array &$items) {
  foreach ($items as $item_id => $item) {
    list(, $raw_id) = \Drupal\search_api\Utility::splitCombinedId($item->getId());
    if ($raw_id % 5 == 0) {
      unset($items[$item_id]);
    }
  }
  drupal_set_message(t('Indexing items on index %index with the following IDs: @ids', array('%index' => $index->label(), '@ids' => implode(', ', array_keys($items)))));
}
 /**
  * Constructs a FieldTrait object.
  *
  * @param \Drupal\search_api\IndexInterface $index
  *   The field's index.
  * @param string $field_identifier
  *   The field's combined identifier, with datasource prefix if applicable.
  */
 public function __construct(IndexInterface $index, $field_identifier)
 {
     $this->index = $index;
     $this->fieldIdentifier = $field_identifier;
     list($this->datasourceId, $this->propertyPath) = Utility::splitCombinedId($field_identifier);
 }
Beispiel #13
0
  /**
   * Tests whether the properties are correctly altered.
   *
   * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedField::alterPropertyDefinitions()
   */
  public function testAlterPropertyDefinitions() {
    $fields = array(
      'entity:test1/foo',
      'entity:test1/foo:bar',
      'entity:test2/foobaz:bla',
    );
    $index_fields = array();
    foreach ($fields as $field_id) {
      $field_object = Utility::createField($this->index, $field_id);
      list($prefix, $label) = str_replace(':', ' » ', Utility::splitCombinedId($field_id));
      $field_object->setLabelPrefix($prefix . ' » ');
      $field_object->setLabel($label);
      $index_fields[$field_id] = $field_object;
    }
    $this->index->expects($this->any())
      ->method('getFields')
      ->will($this->returnValue($index_fields));

    $configuration['fields'] = array(
      'search_api_aggregation_1' => array(
        'label' => 'Field 1',
        'type' => 'union',
        'fields' => array(
          'entity:test1/foo',
          'entity:test1/foo:bar',
          'entity:test2/foobaz:bla',
        ),
      ),
      'search_api_aggregation_2' => array(
        'label' => 'Field 2',
        'type' => 'max',
        'fields' => array(
          'entity:test1/foo:bar',
        ),
      ),
    );
    $this->processor->setConfiguration($configuration);

    /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
    $translation = $this->getStringTranslationStub();
    $this->processor->setStringTranslation($translation);

    // Check for modified properties when no data source is given.
    $properties = array();
    $this->processor->alterPropertyDefinitions($properties, 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.');
    if ($property_added) {
      $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_aggregation_1'], 'The "search_api_aggregation_1" property contains a valid data definition.');
      if ($properties['search_api_aggregation_1'] instanceof DataDefinitionInterface) {
        $this->assertEquals('string', $properties['search_api_aggregation_1']->getDataType(), 'Correct data type set in the data definition.');
        $this->assertEquals('Field 1', $properties['search_api_aggregation_1']->getLabel(), 'Correct label set in the data definition.');
        $description = $translation->translate('A @type aggregation of the following fields: @fields.', array('@type' => 'Union', '@fields' => 'entity » test1 » foo, entity » test1 » foo » bar, entity » test2 » foobaz » bla'));;
        $this->assertEquals($description, $properties['search_api_aggregation_1']->getDescription(), 'Correct description set in the data definition.');
      }
    }

    $property_added = array_key_exists('search_api_aggregation_2', $properties);
    $this->assertTrue($property_added, 'The "search_api_aggregation_2" property was added to the properties.');
    if ($property_added) {
      $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_aggregation_2'], 'The "search_api_aggregation_2" property contains a valid data definition.');
      if ($properties['search_api_aggregation_2'] instanceof DataDefinitionInterface) {
        $this->assertEquals('integer', $properties['search_api_aggregation_2']->getDataType(), 'Correct data type set in the data definition.');
        $this->assertEquals('Field 2', $properties['search_api_aggregation_2']->getLabel(), 'Correct label set in the data definition.');
        $description = $translation->translate('A @type aggregation of the following fields: @fields.', array('@type' => 'Maximum', '@fields' => 'entity » test1 » foo » bar'));;
        $this->assertEquals($description, $properties['search_api_aggregation_2']->getDescription(), 'Correct description set in the data definition.');
      }
    }

    // Test whether the properties of specific datasources stay untouched.
    $properties = array();
    /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
    $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
    $this->processor->alterPropertyDefinitions($properties, $datasource);
    $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
  }
 /**
  * {@inheritdoc}
  */
 public function preprocessIndexItems(array &$items)
 {
     if (!$items) {
         return;
     }
     if (isset($this->configuration['fields'])) {
         /** @var \Drupal\search_api\Item\ItemInterface[] $items */
         foreach ($items as $item) {
             foreach ($this->configuration['fields'] as $aggregated_field_id => $aggregated_field) {
                 if ($aggregated_field['label']) {
                     if (!$item->getField($aggregated_field_id, FALSE)) {
                         continue;
                     }
                     // Extract the selected fields to aggregate from the settings.
                     $required_fields = array();
                     // @todo Don't do this once for every item, compute fields per
                     //   datasource right away.
                     foreach ($aggregated_field['fields'] as $field_id_to_aggregate) {
                         // Only include valid and selected fields to aggregate.
                         if (!isset($required_fields[$field_id_to_aggregate]) && !empty($field_id_to_aggregate)) {
                             // Make sure we only get fields from the datasource of the
                             // current item.
                             list($datasource_id) = Utility::splitCombinedId($field_id_to_aggregate);
                             if (!$datasource_id || $datasource_id == $item->getDatasourceId()) {
                                 $required_fields[$field_id_to_aggregate] = $field_id_to_aggregate;
                             }
                         }
                     }
                     // Get all the available field values.
                     $given_fields = array();
                     foreach ($required_fields as $required_field_id) {
                         $field = $item->getField($required_field_id);
                         if ($field && $field->getValues()) {
                             $given_fields[$required_field_id] = $field;
                             unset($required_fields[$required_field_id]);
                         }
                     }
                     $missing_fields = array();
                     // Get all the missing field values.
                     foreach ($required_fields as $required_field_id) {
                         $field = Utility::createField($this->index, $required_field_id);
                         $missing_fields[$field->getPropertyPath()] = $field;
                     }
                     // Get the value from the original objects in to the fields
                     if ($missing_fields) {
                         Utility::extractFields($item->getOriginalObject(), $missing_fields);
                     }
                     $fields = array_merge($given_fields, $missing_fields);
                     $values = array();
                     /** @var \Drupal\search_api\Item\FieldInterface[] $fields */
                     foreach ($fields as $field) {
                         $values = array_merge($values, $field->getValues());
                     }
                     switch ($aggregated_field['type']) {
                         case 'concat':
                             $values = array(implode("\n\n", $values));
                             break;
                         case 'sum':
                             $values = array(array_sum($values));
                             break;
                         case 'count':
                             $values = array(count($values));
                             break;
                         case 'max':
                             $values = array(max($values));
                             break;
                         case 'min':
                             $values = array(min($values));
                             break;
                         case 'first':
                             if ($values) {
                                 $values = array(reset($values));
                             }
                             break;
                     }
                     $item->getField($aggregated_field_id)->setValues($values);
                 }
             }
         }
     }
 }