/** * Returns the item IDs for the given entity IDs. * * @param array $entity_ids * An array of entity IDs. * * @return string[] * An array of item IDs. */ protected function getItemIds(array $entity_ids) { $translate_ids = function ($entity_id) { return Utility::createCombinedId('entity:entity_test', $entity_id . ':en'); }; return array_map($translate_ids, $entity_ids); }
/** * 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; }
/** * {@inheritdoc} */ public function query() { // If we're not using Field API field rendering, just use the query() // implementation of the fallback handler. if (!$this->options['field_rendering']) { $this->fallbackHandler->query(); return; } // If we do use Field API rendering, we need the entity object for the // parent property. $parent_path = $this->getParentPath(); $property_path = $parent_path ? "{$parent_path}:_object" : '_object'; $combined_property_path = Utility::createCombinedId($this->getDatasourceId(), $property_path); $this->addRetrievedProperty($combined_property_path); }
/** * Creates a database query condition for a given search filter. * * Used as a helper method in createDbQuery(). * * @param \Drupal\search_api\Query\FilterInterface $filter * The filter for which a condition should be created. * @param array $fields * Internal information about the index's fields. * @param \Drupal\Core\Database\Query\SelectInterface $db_query * The database query to which the condition will be added. * @param \Drupal\search_api\IndexInterface $index * The index we're searching on. * * @return \Drupal\Core\Database\Query\ConditionInterface|null * The condition to set on the query, or NULL if none is necessary. * * @throws \Drupal\search_api\SearchApiException * Thrown if an unknown field was used in the filter. */ protected function createFilterCondition(FilterInterface $filter, array $fields, SelectInterface $db_query, IndexInterface $index) { $cond = new Condition($filter->getConjunction()); $db_info = $this->getIndexDbInfo($index); $empty = TRUE; // Store whether a JOIN already occurred for a field, so we don't JOIN // repeatedly for OR filters. $first_join = array(); // Store the table aliases for the fields in this condition group. $tables = array(); foreach ($filter->getFilters() as $f) { if (is_object($f)) { $c = $this->createFilterCondition($f, $fields, $db_query, $index); if ($c) { $empty = FALSE; $cond->condition($c); } } else { $empty = FALSE; // We don't index the datasource explicitly, so this needs a bit of // magic. // @todo Index the datasource explicitly so this doesn't need magic. if ($f[0] === 'search_api_datasource') { $alias = $this->getTableAlias(array('table' => $db_info['index_table']), $db_query); // @todo Stop recognizing "!=" as an operator. $operator = ($f[2] == '<>' || $f[2] == '!=') ? 'NOT LIKE' : 'LIKE'; $prefix = Utility::createCombinedId($f[1], ''); $cond->condition($alias . '.item_id', $this->database->escapeLike($prefix) . '%', $operator); continue; } if (!isset($fields[$f[0]])) { throw new SearchApiException(new FormattableMarkup('Unknown field in filter clause: @field.', array('@field' => $f[0]))); } $field = $fields[$f[0]]; // Fields have their own table, so we have to check for NULL values in // a special way (i.e., check for missing entries in that table). // @todo This can probably always use the denormalized table. if ($f[1] === NULL) { $query = $this->database->select($field['table'], 't') ->fields('t', array('item_id')); $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'IN' : 'NOT IN'); continue; } if (Utility::isTextType($field['type'])) { $keys = $this->prepareKeys($f[1]); $query = $this->createKeysQuery($keys, array($f[0] => $field), $fields, $index); // We don't need the score, so we remove it. The score might either be // an expression or a field. $query_expressions = &$query->getExpressions(); if ($query_expressions) { $query_expressions = array(); } else { $query_fields = &$query->getFields(); unset($query_fields['score']); unset($query_fields); } unset($query_expressions); $cond->condition('t.item_id', $query, $f[2] == '<>' || $f[2] == '!=' ? 'NOT IN' : 'IN'); } else { $new_join = ($filter->getConjunction() == 'AND' || empty($first_join[$f[0]])); if ($new_join || empty($tables[$f[0]])) { $tables[$f[0]] = $this->getTableAlias($field, $db_query, $new_join); $first_join[$f[0]] = TRUE; } $column = $tables[$f[0]] . '.' . 'value'; if ($f[1] !== NULL) { $cond->condition($column, $f[1], $f[2]); } else { $method = ($f[2] == '=') ? 'isNull' : 'isNotNull'; $cond->$method($column); } } } } return $empty ? NULL : $cond; }
/** * {@inheritdoc} */ public function getCombinedPropertyPath() { return Utility::createCombinedId($this->getDatasourceId(), $this->getPropertyPath()); }
/** * {@inheritdoc} */ public function search(QueryInterface $query) { $this->checkError(__FUNCTION__); $results = Utility::createSearchResultSet($query); $result_items = array(); $datasources = $query->getIndex()->getDatasources(); /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */ $datasource = reset($datasources); $datasource_id = $datasource->getPluginId(); if ($query->getKeys() && $query->getKeys()[0] == 'test') { $item_id = Utility::createCombinedId($datasource_id, '1'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(2); $item->setExcerpt('test'); $result_items[$item_id] = $item; } elseif ($query->getOption('search_api_mlt')) { $item_id = Utility::createCombinedId($datasource_id, '2'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(2); $item->setExcerpt('test test'); $result_items[$item_id] = $item; } else { $item_id = Utility::createCombinedId($datasource_id, '1'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(1); $result_items[$item_id] = $item; $item_id = Utility::createCombinedId($datasource_id, '2'); $item = Utility::createItem($query->getIndex(), $item_id, $datasource); $item->setScore(1); $result_items[$item_id] = $item; } $results->setResultCount(count($result_items)); return $results; }
/** * Creates an items list for the given properties. * * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties * The property definitions, keyed by their property names. * @param string $active_property_path * The relative property path to the active property. * @param \Drupal\Core\Url $base_url * The base URL to which property path parameters should be added for * the navigation links. * @param string $parent_path * (optional) The common property path prefix of the given properties. * @param string $label_prefix * (optional) The prefix to use for the labels of created fields. * * @return array * A render array representing the given properties and, possibly, nested * properties. */ protected function getPropertiesList(array $properties, $active_property_path, Url $base_url, $parent_path = '', $label_prefix = '') { $list = array('#theme' => 'search_api_form_item_list'); $active_item = ''; if ($active_property_path) { list($active_item, $active_property_path) = explode(':', $active_property_path, 2) + array(1 => ''); } $type_mapping = Utility::getFieldTypeMapping(); $query_base = $base_url->getOption('query'); foreach ($properties as $key => $property) { $this_path = $parent_path ? $parent_path . ':' : ''; $this_path .= $key; $label = $property->getLabel(); $property = Utility::getInnerProperty($property); $can_be_indexed = TRUE; $nested_properties = array(); $parent_child_type = NULL; if ($property instanceof ComplexDataDefinitionInterface) { $can_be_indexed = FALSE; $nested_properties = $property->getPropertyDefinitions(); $main_property = $property->getMainPropertyName(); if ($main_property && isset($nested_properties[$main_property])) { $parent_child_type = $property->getDataType() . '.'; $property = $nested_properties[$main_property]; $parent_child_type .= $property->getDataType(); unset($nested_properties[$main_property]); $can_be_indexed = TRUE; } // Don't add the additional 'entity' property for entity reference // fields which don't target a content entity type. if ($property instanceof FieldItemDataDefinition && in_array($property->getDataType(), array('field_item:entity_reference', 'field_item:image', 'field_item:file'))) { $entity_type = $this->getEntityTypeManager()->getDefinition($property->getSetting('target_type')); if (!$entity_type->isSubclassOf('Drupal\\Core\\Entity\\ContentEntityInterface')) { unset($nested_properties['entity']); } } } // Don't allow indexing of properties with unmapped types. Also, prefer // a "parent.child" type mapping (taking into account the parent property // for, e.g., text fields). $type = $property->getDataType(); if ($parent_child_type && !empty($type_mapping[$parent_child_type])) { $type = $parent_child_type; } elseif (empty($type_mapping[$type])) { // Remember the type only if it was not explicitly mapped to FALSE. if (!isset($type_mapping[$type])) { $this->unmappedFields[$type][] = $label_prefix . $label; } $can_be_indexed = FALSE; } // If the property can neither be expanded nor indexed, just skip it. if (!($nested_properties || $can_be_indexed)) { continue; } $nested_list = array(); $expand_link = array(); if ($nested_properties) { if ($key == $active_item) { $link_url = clone $base_url; $query_base['property_path'] = $parent_path; $link_url->setOption('query', $query_base); $expand_link = array('#type' => 'link', '#title' => '(-) ', '#url' => $link_url); $nested_list = $this->getPropertiesList($nested_properties, $active_property_path, $base_url, $this_path, $label_prefix . $label . ' » '); } else { $link_url = clone $base_url; $query_base['property_path'] = $this_path; $link_url->setOption('query', $query_base); $expand_link = array('#type' => 'link', '#title' => '(+) ', '#url' => $link_url); } } $item = array('#type' => 'container', '#attributes' => array('class' => array('container-inline'))); if ($expand_link) { $item['expand_link'] = $expand_link; } $item['label']['#markup'] = Html::escape($label) . ' '; if ($can_be_indexed) { $item['add'] = array('#type' => 'submit', '#name' => Utility::createCombinedId($this->getParameter('datasource') ?: NULL, $this_path), '#value' => $this->t('Add'), '#submit' => array('::addField', '::save'), '#property' => $property, '#prefixed_label' => $label_prefix . $label, '#data_type' => $type_mapping[$type]); } if ($nested_list) { $item['properties'] = $nested_list; } $list[] = $item; } return $list; }
/** * Retrieve all properties available on the index. * * The properties will be keyed by combined ID, which is a combination of the * 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) { $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) { $properties[Utility::createCombinedId($datasource_id, $property_path)] = $property; } } return $properties; }
/** * Generates some test items. * * @param array[] $items * Array of items to be transformed into proper search item objects. Each * item in this array is an associative array with the following keys: * - datasource: The datasource plugin ID. * - item: The item object to be indexed. * - item_id: The datasource-specific raw item ID. * - *: Any other keys will be treated as property paths, and their values * as a single value for a field with that property path. * * @return \Drupal\search_api\Item\ItemInterface[] * The generated test items. */ public function generateItems(array $items) { /** @var \Drupal\search_api\Item\ItemInterface[] $extracted_items */ $extracted_items = array(); foreach ($items as $item) { $id = Utility::createCombinedId($item['datasource'], $item['item_id']); $extracted_items[$id] = Utility::createItemFromObject($this->index, $item['item'], $id); foreach (array(NULL, $item['datasource']) as $datasource_id) { foreach ($this->index->getFieldsByDatasource($datasource_id) as $key => $field) { /** @var \Drupal\search_api\Item\FieldInterface $field */ $field = clone $field; if (isset($item[$field->getPropertyPath()])) { $field->addValue($item[$field->getPropertyPath()]); } $extracted_items[$id]->setField($key, $field); } } } return $extracted_items; }
/** * Asserts that the search results contain the expected IDs. * * @param ResultSetInterface $result * The search results. * @param int[][] $ids * The expected entity IDs, grouped by entity type and with their indexes in * this object's respective array properties as the values. */ protected function assertResults(ResultSetInterface $result, array $expected) { $results = array_keys($result->getResultItems()); sort($results); $ids = array(); foreach ($expected as $entity_type => $items) { $datasource_id = "entity:{$entity_type}"; foreach ($items as $i) { if ($entity_type == 'user') { $id = $i . ':en'; } else { $id = $this->{"{$entity_type}s"}[$i]->id() . ':en'; } $ids[] = Utility::createCombinedId($datasource_id, $id); } } sort($ids); $this->assertEqual($results, $ids); }
/** * Adds a node access filter to a search query, if applicable. * * @param \Drupal\search_api\Query\QueryInterface $query * The query to which a node access filter should be added, if applicable. * @param \Drupal\Core\Session\AccountInterface $account * The user for whom the search is executed. * * @throws \Drupal\search_api\SearchApiException * Thrown if not all necessary fields are indexed on the index. */ protected function addNodeAccess(QueryInterface $query, AccountInterface $account) { // Don't do anything if the user can access all content. if ($account->hasPermission('bypass node access')) { return; } // Gather the affected datasources, grouped by entity type, as well as the // unaffected ones. $affected_datasources = array(); $unaffected_datasources = array(); foreach ($this->index->getDatasources() as $datasource_id => $datasource) { $entity_type = $datasource->getEntityTypeId(); if (in_array($entity_type, array('node', 'comment'))) { $affected_datasources[$entity_type][] = $datasource_id; } else { $unaffected_datasources[] = $datasource_id; } } // The filter structure we want looks like this: // [belongs to other datasource] // OR // ( // [is enabled (or was created by the user, if applicable)] // AND // [grants view access to one of the user's gid/realm combinations] // ) // If there are no "other" datasources, we don't need the nested OR, // however, and can add the "ADD" // @todo Add a filter tag, once they are implemented. if ($unaffected_datasources) { $outer_filter = $query->createFilter('OR'); $query->filter($outer_filter); foreach ($unaffected_datasources as $datasource_id) { $outer_filter->condition('search_api_datasource', $datasource_id); } $access_filter = $query->createFilter('AND'); $outer_filter->filter($access_filter); } else { $access_filter = $query; } if (!$account->hasPermission('access content')) { unset($affected_datasources['node']); } if (!$account->hasPermission('access comments')) { unset($affected_datasources['comment']); } // If the user does not have the permission to see any content at all, deny // access to all items from affected datasources. if (!$affected_datasources) { // If there were "other" datasources, the existing filter will already // remove all results of node or comment datasources. Otherwise, we should // not return any results at all. if (!$unaffected_datasources) { // @todo More elegant way to return no results? $query->condition('search_api_language', ''); } return; } // Collect all the required fields that need to be part of the index. $unpublished_own = $account->hasPermission('view own unpublished content'); $enabled_filter = $query->createFilter('OR'); foreach ($affected_datasources as $entity_type => $datasources) { $published = $entity_type == 'node' ? NODE_PUBLISHED : Comment::PUBLISHED; foreach ($datasources as $datasource_id) { // If this is a comment datasource, or users cannot view their own // unpublished nodes, a simple filter on "status" is enough. Otherwise, // it's a bit more complicated. $status_field = Utility::createCombinedId($datasource_id, 'status'); $enabled_filter->condition($status_field, $published); if ($entity_type == 'node' && $unpublished_own) { $author_field = Utility::createCombinedId($datasource_id, 'uid'); $enabled_filter->condition($author_field, $account->id()); } } } $access_filter->filter($enabled_filter); // Filter by the user's node access grants. $grants_filter = $query->createFilter('OR'); $grants = node_access_grants('view', $account); foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { $grants_filter->condition('search_api_node_grants', "node_access_{$realm}:{$gid}"); } } // Also add items that are accessible for everyone by checking the "access // all" pseudo grant. $grants_filter->condition('search_api_node_grants', 'node_access__all'); $access_filter->filter($grants_filter); }
/** * Retrieves this field's label. * * The field's label, contrary to the label returned by the field's data * definition, contains a human-readable representation of the full property * path. The datasource label is not included, though – use getPrefixedLabel() * for that. * * @return string * A human-readable label representing this field's property path. * * @see \Drupal\search_api\Item\GenericFieldInterface::getLabel() */ public function getLabel() { if (!isset($this->label)) { $label = ''; try { $label = $this->getDataDefinition()->getLabel(); } catch (SearchApiException $e) { watchdog_exception('search_api', $e); } $pos = strrpos($this->propertyPath, ':'); if ($pos) { $parent_id = substr($this->propertyPath, 0, $pos); if ($this->datasourceId) { $parent_id = Utility::createCombinedId($this->datasourceId, $parent_id); } $label = Utility::createField($this->index, $parent_id)->getLabel() . ' » ' . $label; } $this->label = $label; } return $this->label; }
/** * Tests field highlighting and excerpts with two items. */ public function testPostprocessSearchResultsWithTwoItems() { $body_field_id = Utility::createCombinedId('entity:node', 'body'); $query = $this->getMock('Drupal\search_api\Query\QueryInterface'); $query->expects($this->atLeastOnce()) ->method('getKeys') ->will($this->returnValue(array('#conjunction' => 'OR', 'foo'))); /** @var \Drupal\search_api\Query\QueryInterface $query */ /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */ $index = $this->getMock('Drupal\search_api\IndexInterface'); $body_field = Utility::createField($index, $body_field_id); $body_field->setType('text'); $index->expects($this->atLeastOnce()) ->method('getFields') ->will($this->returnValue(array($body_field_id => $body_field))); $this->processor->setIndex($index); $body_values = array('Some foo value', 'foo bar'); $fields = array( $body_field_id => array( 'type' => 'text', 'values' => $body_values, ), ); $items = $this->createItems($index, 2, $fields); $items[$this->itemIds[1]]->getField($body_field_id)->setValues(array('The second item also contains foo in its body.')); $results = Utility::createSearchResultSet($query); $results->setResultItems($items); $results->setResultCount(1); $this->processor->postprocessSearchResults($results); $output = $results->getExtraData('highlighted_fields'); $this->assertEquals('Some <strong>foo</strong> value', $output[$this->itemIds[0]][$body_field_id][0], 'Highlighting is correctly applied to first body field value.'); $this->assertEquals('<strong>foo</strong> bar', $output[$this->itemIds[0]][$body_field_id][1], 'Highlighting is correctly applied to second body field value.'); $this->assertEquals('The second item also contains <strong>foo</strong> in its body.', $output[$this->itemIds[1]][$body_field_id][0], 'Highlighting is correctly applied to second item.'); $excerpt1 = '… Some <strong>foo</strong> value … <strong>foo</strong> bar …'; $excerpt2 = '… The second item also contains <strong>foo</strong> in its body. …'; $this->assertEquals($excerpt1, $items[$this->itemIds[0]]->getExcerpt(), 'Correct excerpt created from two text fields.'); $this->assertEquals($excerpt2, $items[$this->itemIds[1]]->getExcerpt(), 'Correct excerpt created for second item.'); }
/** * Tests preprocessing search items with an exclusive filter. */ public function testFilterExclusive() { $configuration['roles'] = array('editor' => 'editor'); $configuration['default'] = 1; $this->processor->setConfiguration($configuration); $this->processor->preprocessIndexItems($this->items); $this->assertTrue(empty($this->items[Utility::createCombinedId('entity:user', '1:en')]), 'User with editor role was successfully removed.'); $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:user', '2:en')]), 'User without the editor role was not removed.'); $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:node', '1:en')]), 'Node item was not removed.'); }
/** * Creates a database query condition for a given search filter. * * Used as a helper method in createDbQuery(). * * @param \Drupal\search_api\Query\ConditionGroupInterface $conditions * The conditions for which a condition should be created. * @param array $fields * Internal information about the index's fields. * @param \Drupal\Core\Database\Query\SelectInterface $db_query * The database query to which the condition will be added. * @param \Drupal\search_api\IndexInterface $index * The index we're searching on. * * @return \Drupal\Core\Database\Query\ConditionInterface|null * The condition to set on the query, or NULL if none is necessary. * * @throws \Drupal\search_api\SearchApiException * Thrown if an unknown field was used in the filter. */ protected function createDbCondition(ConditionGroupInterface $conditions, array $fields, SelectInterface $db_query, IndexInterface $index) { $db_condition = new Condition($conditions->getConjunction()); $db_info = $this->getIndexDbInfo($index); // Store whether a JOIN already occurred for a field, so we don't JOIN // repeatedly for OR filters. $first_join = array(); // Store the table aliases for the fields in this condition group. $tables = array(); foreach ($conditions->getConditions() as $condition) { if ($condition instanceof ConditionGroupInterface) { $sub_condition = $this->createDbCondition($condition, $fields, $db_query, $index); if ($sub_condition) { $db_condition->condition($sub_condition); } } else { $field = $condition->getField(); $operator = $condition->getOperator(); $value = $condition->getValue(); $not_equals = $operator == '<>' || $operator == '!='; // We don't index the datasource explicitly, so this needs a bit of // magic. // @todo Index the datasource explicitly so this doesn't need magic. if ($field === 'search_api_datasource') { if (empty($tables[NULL])) { $table = array('table' => $db_info['index_table']); $tables[NULL] = $this->getTableAlias($table, $db_query); } $operator = $not_equals ? 'NOT LIKE' : 'LIKE'; $prefix = Utility::createCombinedId($value, ''); $db_condition->condition($tables[NULL] . '.item_id', $this->database->escapeLike($prefix) . '%', $operator); continue; } if (!isset($fields[$field])) { throw new SearchApiException(new FormattableMarkup('Unknown field in filter clause: @field.', array('@field' => $field))); } $field_info = $fields[$field]; // For NULL values, we can just use the single-values table, since we // only need to know if there's any value at all for that field. if ($value === NULL || empty($field_info['multi-valued'])) { if (empty($tables[NULL])) { $table = array('table' => $db_info['index_table']); $tables[NULL] = $this->getTableAlias($table, $db_query); } $column = $tables[NULL] . '.' . $field_info['column']; if ($value === NULL) { $method = $not_equals ? 'isNotNull' : 'isNull'; $db_condition->{$method}($column); } else { $db_condition->condition($column, $value, $operator); } continue; } if (Utility::isTextType($field_info['type'])) { $keys = $this->prepareKeys($value); if (!isset($keys)) { continue; } $query = $this->createKeysQuery($keys, array($field => $field_info), $fields, $index); // We only want the item IDs, so we use the keys query as a nested // query. $query = $this->database->select($query, 't')->fields('t', array('item_id')); $db_condition->condition('t.item_id', $query, $not_equals ? 'NOT IN' : 'IN'); } else { $new_join = $conditions->getConjunction() == 'AND' || empty($first_join[$field]); if ($new_join || empty($tables[$field])) { $tables[$field] = $this->getTableAlias($field_info, $db_query, $new_join); $first_join[$field] = TRUE; } $column = $tables[$field] . '.' . 'value'; if ($not_equals) { // The situation is more complicated for multi-valued fields, since // we must make sure that results are excluded if ANY of the field's // values equals the one given in this condition. $query = $this->database->select($field_info['table'], 't')->fields('t', array('item_id'))->condition('value', $value); $db_condition->condition('t.item_id', $query, 'NOT IN'); } else { $db_condition->condition($column, $value, $operator); } } } } return $db_condition->count() ? $db_condition : NULL; }
/** * {@inheritdoc} */ public function trackItemsDeleted($datasource_id, array $ids) { if ($this->hasValidTracker() && $this->status()) { $item_ids = array(); foreach ($ids as $id) { $item_ids[] = Utility::createCombinedId($datasource_id, $id); } $this->getTrackerInstance()->trackItemsDeleted($item_ids); if (!$this->isReadOnly() && $this->isServerEnabled()) { $this->getServerInstance()->deleteItems($this, $item_ids); } } }
/** * {@inheritdoc} */ public function addItemsOnce(IndexInterface $index) { $index_state = $this->getIndexState($index); if (!($index_state['status'] && $index_state['pages'])) { return NULL; } if (!$index->hasValidTracker()) { return 0; } // Use this method to automatically circle through the datasources, adding // items from each of them in turn. $page = reset($index_state['pages']); $datasource_id = key($index_state['pages']); unset($index_state['pages'][$datasource_id]); $added = 0; if ($index->isValidDatasource($datasource_id)) { $raw_ids = $index->getDatasource($datasource_id)->getItemIds($page); if ($raw_ids !== NULL) { $index_state['pages'][$datasource_id] = ++$page; if ($raw_ids) { $item_ids = array(); foreach ($raw_ids as $raw_id) { $item_ids[] = Utility::createCombinedId($datasource_id, $raw_id); } $added = count($item_ids); $index->getTrackerInstance()->trackItemsInserted($item_ids); } } } if (empty($index_state['pages'])) { $this->state->delete($this->getIndexStateKey($index)); return NULL; } $this->state->set($this->getIndexStateKey($index), $index_state); return $added; }
/** * 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; }
/** * Adds a field for a specific property to the index. * * @param string|null $datasource_id * The property's datasource's ID, or NULL if it is a datasource-independent * property. * @param string $property_path * The property path. * @param string|null $label * (optional) If given, the label to check for in the success message. */ protected function addField($datasource_id, $property_path, $label = NULL) { $path = $this->getIndexPath('fields/add'); $url_options = array('query' => array('datasource' => $datasource_id)); if ($this->getUrl() === $this->buildUrl($path, $url_options)) { $path = NULL; } // Unfortunately it doesn't seem possible to specify the clicked button by // anything other than label, so we have to pass it as extra POST data. $combined_property_path = Utility::createCombinedId($datasource_id, $property_path); $post = '&' . $this->serializePostValues(array($combined_property_path => $this->t('Add'))); $this->drupalPostForm($path, array(), NULL, $url_options, array(), NULL, $post); if ($label) { $args['%label'] = $label; $this->assertRaw($this->t('Field %label was added to the index.', $args)); } }
/** * Tests whether indexed items are correctly preprocessed. */ public function testProcessIndexItems() { /** @var \Drupal\node\Entity\Node $node */ $node = $this->getMockBuilder('Drupal\node\Entity\Node') ->disableOriginalConstructor() ->getMock(); $body_value = array('Some text value'); $body_field_id = Utility::createCombinedId('entity:node', 'body'); $fields = array( 'search_api_url' => array( 'type' => 'string' ), $body_field_id => array( 'type' => 'text', 'values' => $body_value, ), ); $items = $this->createItems($this->index, 2, $fields, EntityAdapter::createFromEntity($node)); // Process the items. $this->processor->preprocessIndexItems($items); // Check the valid item. $field = $items[$this->itemIds[0]]->getField('search_api_url'); $this->assertEquals(array('http://www.example.com/node/example'), $field->getValues(), 'Valid URL added as value to the field.'); // Check that no other fields were changed. $field = $items[$this->itemIds[0]]->getField($body_field_id); $this->assertEquals($body_value, $field->getValues(), 'Body field was not changed.'); // Check the second item to be sure that all are processed. $field = $items[$this->itemIds[1]]->getField('search_api_url'); $this->assertEquals(array('http://www.example.com/node/example'), $field->getValues(), 'Valid URL added as value to the field in the second item.'); }
/** * Expands the properties to retrieve for this field. * * The properties are taken from this object's $retrievedProperties property, * with all their ancestors also added to the array, with the ancestor * properties always ordered before their descendants. * * This will ensure, when dealing with these properties sequentially, that * the parent object necessary to load the "child" property is always already * loaded. * * @return string[][] * The combined property paths to retrieve, keyed by their datasource ID and * property path. */ protected function expandRequiredProperties() { $required_properties = array(); foreach ($this->retrievedProperties as $datasource_id => $properties) { foreach (array_keys($properties) as $property_path) { $path_to_add = ''; foreach (explode(':', $property_path) as $component) { $path_to_add .= ($path_to_add ? ':' : '') . $component; if (!isset($required_properties[$path_to_add])) { $required_properties[$datasource_id][$path_to_add] = Utility::createCombinedId($datasource_id, $path_to_add); } } } } return $required_properties; }
/** * Tests if unpublished nodes are removed from the items list. */ public function testNodeStatus() { $this->assertCount(2, $this->items, '2 nodes in the index.'); $this->processor->preprocessIndexItems($this->items); $this->assertCount(1, $this->items, 'An item was removed from the items list.'); $published_nid = Utility::createCombinedId('entity:node', '2:en'); $this->assertTrue(isset($this->items[$published_nid]), 'Correct item was removed.'); }
/** * Ensures that a field with certain properties is indexed on the index. * * Can be used as a helper method in preIndexSave(). * * @param string|null $datasource_id * The ID of the field's datasource, or NULL for a datasource-independent * field. * @param string $property_path * The field's property path on the datasource. * @param string|null $type * (optional) If set, the field should have this type. * * @return \Drupal\search_api\Item\FieldInterface * A field on the index, possibly newly added, with the specified * properties. * * @throws \Drupal\search_api\SearchApiException * Thrown if there is no property with the specified path, or no type is * given and no default could be determined for the property. */ protected function ensureField($datasource_id, $property_path, $type = NULL) { $field = $this->findField($datasource_id, $property_path, $type); if (!$field) { $property = Utility::retrieveNestedProperty($this->index->getPropertyDefinitions($datasource_id), $property_path); if (!$property) { $args['%property'] = Utility::createCombinedId($datasource_id, $property_path); $args['%processor'] = $this->label(); $message = new FormattableMarkup('Could not find property %property which is required by the %processor processor.', $args); throw new SearchApiException($message); } $field = Utility::createFieldFromProperty($this->index, $property, $datasource_id, $property_path, NULL, $type); } $field->setIndexedLocked(); if (isset($type)) { $field->setTypeLocked(); } $this->index->addField($field); return $field; }