search_api-8.x-1.15/src/Item/Item.php
src/Item/Item.php
<?php
namespace Drupal\search_api\Item;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Processor\ProcessorInterface;
use Drupal\search_api\Processor\ProcessorPropertyInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
/**
* Provides a default implementation for a search item.
*/
class Item implements \IteratorAggregate, ItemInterface {
use LoggerTrait;
/**
* The search index with which this item is associated.
*
* @var \Drupal\search_api\IndexInterface
*/
protected $index;
/**
* The ID of this item.
*
* @var string
*/
protected $itemId;
/**
* The complex data item this Search API item is based on.
*
* @var \Drupal\Core\TypedData\ComplexDataInterface
*/
protected $originalObject;
/**
* The ID of this item's datasource.
*
* @var string
*/
protected $datasourceId;
/**
* The datasource of this item.
*
* @var \Drupal\search_api\Datasource\DatasourceInterface
*/
protected $datasource;
/**
* The language code of this item.
*
* @var string
*/
protected $language;
/**
* The extracted fields of this item.
*
* @var \Drupal\search_api\Item\FieldInterface[]
*/
protected $fields = [];
/**
* Whether the fields were already extracted for this item.
*
* @var bool
*/
protected $fieldsExtracted = FALSE;
/**
* The HTML text with highlighted text-parts that match the query.
*
* @var string
*/
protected $excerpt;
/**
* The score this item had as a result in a corresponding search query.
*
* @var float
*/
protected $score = 1.0;
/**
* The boost of this item at indexing time.
*
* @var float
*/
protected $boost = 1.0;
/**
* Extra data set on this item.
*
* @var array
*/
protected $extraData = [];
/**
* Cached access results for the item, keyed by user ID.
*
* @var \Drupal\Core\Access\AccessResultInterface[]
*
* @see getAccessResult()
*/
protected $accessResults = [];
/**
* Constructs an Item object.
*
* @param \Drupal\search_api\IndexInterface $index
* The item's search index.
* @param string $id
* The ID of this item.
* @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
* (optional) The datasource of this item. If not set, it will be determined
* from the ID and loaded from the index.
*/
public function __construct(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
$this->index = $index;
$this->itemId = $id;
if ($datasource) {
$this->datasource = $datasource;
$this->datasourceId = $datasource->getPluginId();
}
}
/**
* {@inheritdoc}
*/
public function getDatasourceId() {
if (!isset($this->datasourceId)) {
list($this->datasourceId) = Utility::splitCombinedId($this->itemId);
}
return $this->datasourceId;
}
/**
* {@inheritdoc}
*/
public function getDatasource() {
if (!isset($this->datasource)) {
$this->datasource = $this->index->getDatasource($this->getDatasourceId());
}
return $this->datasource;
}
/**
* {@inheritdoc}
*/
public function getIndex() {
return $this->index;
}
/**
* {@inheritdoc}
*/
public function getLanguage() {
if (!isset($this->language)) {
$this->language = $this->getDatasource()->getItemLanguage($this->getOriginalObject());
}
return $this->language;
}
/**
* {@inheritdoc}
*/
public function setLanguage($language) {
$this->language = $language;
return $this;
}
/**
* {@inheritdoc}
*/
public function getId() {
return $this->itemId;
}
/**
* {@inheritdoc}
*/
public function getOriginalObject($load = TRUE) {
if (!isset($this->originalObject) && $load) {
$this->originalObject = $this->index->loadItem($this->itemId);
if (!$this->originalObject) {
throw new SearchApiException('Failed to load original object ' . $this->itemId);
}
}
return $this->originalObject;
}
/**
* {@inheritdoc}
*/
public function setOriginalObject(ComplexDataInterface $original_object) {
$this->originalObject = $original_object;
return $this;
}
/**
* {@inheritdoc}
*/
public function getField($field_id, $extract = TRUE) {
if (isset($this->fields[$field_id])) {
return $this->fields[$field_id];
}
$fields = $this->getFields($extract);
return isset($fields[$field_id]) ? $fields[$field_id] : NULL;
}
/**
* {@inheritdoc}
*/
public function getFields($extract = TRUE) {
if ($extract && !$this->fieldsExtracted) {
$data_type_fallback_mapping = \Drupal::getContainer()
->get('search_api.data_type_helper')
->getDataTypeFallbackMapping($this->index);
foreach ([NULL, $this->getDatasourceId()] as $datasource_id) {
$fields_by_property_path = [];
$processors_with_fields = [];
$properties = $this->index->getPropertyDefinitions($datasource_id);
foreach ($this->index->getFieldsByDatasource($datasource_id) as $field_id => $field) {
// Don't overwrite fields that were previously set.
if (empty($this->fields[$field_id])) {
$this->fields[$field_id] = clone $field;
$field_data_type = $this->fields[$field_id]->getType();
// If the field data type is in the fallback mapping list, then use
// the fallback type as field type.
if (isset($data_type_fallback_mapping[$field_data_type])) {
$this->fields[$field_id]->setType($data_type_fallback_mapping[$field_data_type]);
}
// For determining whether the field is provided via a processor, we
// need to check using the first part of its property path (in other
// words, the property that's directly on the result item, not
// nested), since only direct properties of the item can be added by
// the processor.
$property = NULL;
$property_name = Utility::splitPropertyPath($field->getPropertyPath(), FALSE)[0];
if (isset($properties[$property_name])) {
$property = $properties[$property_name];
}
if ($property instanceof ProcessorPropertyInterface) {
$processors_with_fields[$property->getProcessorId()] = TRUE;
}
elseif ($datasource_id) {
$fields_by_property_path[$field->getPropertyPath()][] = $this->fields[$field_id];
}
}
}
try {
if ($fields_by_property_path) {
\Drupal::getContainer()
->get('search_api.fields_helper')
->extractFields($this->getOriginalObject(), $fields_by_property_path, $this->getLanguage());
}
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.
$this->logException($e);
}
}
$this->fieldsExtracted = TRUE;
}
return $this->fields;
}
/**
* {@inheritdoc}
*/
public function setField($field_id, FieldInterface $field = NULL) {
if ($field) {
if ($field->getFieldIdentifier() !== $field_id) {
throw new \InvalidArgumentException('The field identifier passed must be consistent with the identifier set on the field object.');
}
// Make sure that the field has the same index object set as we. This
// might otherwise cause impossibly hard-to-detect bugs.
$field->setIndex($this->index);
$this->fields[$field_id] = $field;
}
else {
unset($this->fields[$field_id]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setFields(array $fields) {
// Make sure that all fields have the same index object set as we. This
// might otherwise cause impossibly hard-to-detect bugs.
/** @var \Drupal\search_api\Item\FieldInterface $field */
foreach ($fields as $field) {
$field->setIndex($this->index);
}
$this->fields = $fields;
return $this;
}
/**
* {@inheritdoc}
*/
public function isFieldsExtracted() {
return $this->fieldsExtracted;
}
/**
* {@inheritdoc}
*/
public function setFieldsExtracted($fields_extracted) {
$this->fieldsExtracted = $fields_extracted;
return $this;
}
/**
* {@inheritdoc}
*/
public function getScore() {
return $this->score;
}
/**
* {@inheritdoc}
*/
public function setScore($score) {
$this->score = $score;
return $this;
}
/**
* {@inheritdoc}
*/
public function getBoost() {
return $this->boost;
}
/**
* {@inheritdoc}
*/
public function setBoost($boost) {
$this->boost = $boost;
return $this;
}
/**
* {@inheritdoc}
*/
public function getExcerpt() {
return $this->excerpt;
}
/**
* {@inheritdoc}
*/
public function setExcerpt($excerpt) {
$this->excerpt = $excerpt;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasExtraData($key) {
return array_key_exists($key, $this->extraData);
}
/**
* {@inheritdoc}
*/
public function getExtraData($key, $default = NULL) {
return array_key_exists($key, $this->extraData) ? $this->extraData[$key] : $default;
}
/**
* {@inheritdoc}
*/
public function getAllExtraData() {
return $this->extraData;
}
/**
* {@inheritdoc}
*/
public function setExtraData($key, $data = NULL) {
if (isset($data)) {
$this->extraData[$key] = $data;
}
else {
unset($this->extraData[$key]);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function checkAccess(AccountInterface $account = NULL) {
@trigger_error('\Drupal\search_api\Item\ItemInterface::checkAccess() is deprecated in search_api:8.x-1.14 and is removed from search_api:9.x-1.0. Use getAccessResult() instead. See https://www.drupal.org/node/3051902', E_USER_DEPRECATED);
return $this->getAccessResult($account)->isAllowed();
}
/**
* {@inheritdoc}
*/
public function getAccessResult(AccountInterface $account = NULL) {
if (!$account) {
$account = \Drupal::currentUser();
}
$uid = $account->id();
if (empty($this->accessResults[$uid])) {
try {
$this->accessResults[$uid] = $this->getDatasource()
->getItemAccessResult($this->getOriginalObject(), $account);
}
catch (SearchApiException $e) {
$this->accessResults[$uid] = AccessResult::neutral('Item could not be loaded, so cannot check access');
}
}
return $this->accessResults[$uid];
}
/**
* {@inheritdoc}
*/
public function getIterator() {
return new \ArrayIterator($this->getFields());
}
/**
* Implements the magic __clone() method to implement a deep clone.
*/
public function __clone() {
// The fields definitely need to be cloned. For the extra data its hard (or,
// rather, impossible) to tell, but we opt for cloning objects there, too,
// to be on the (hopefully) safer side. (Ideas for later: introduce an
// interface that tells us to not clone the data object; or check whether
// its an entity; or introduce some other system to override this default.)
foreach ($this->fields as $field_id => $field) {
$this->fields[$field_id] = clone $field;
}
foreach ($this->extraData as $key => $data) {
if (is_object($data)) {
$this->extraData[$key] = clone $data;
}
}
}
/**
* Implements the magic __toString() method to simplify debugging.
*/
public function __toString() {
$out = 'Item ' . $this->getId();
if ($this->getScore() != 1) {
$out .= "\nScore: " . $this->getScore();
}
if ($this->getBoost() != 1) {
$out .= "\nBoost: " . $this->getBoost();
}
if ($this->getExcerpt()) {
$excerpt = str_replace("\n", "\n ", $this->getExcerpt());
$out .= "\nExcerpt: $excerpt";
}
if ($this->getFields(FALSE)) {
$out .= "\nFields:";
foreach ($this->getFields(FALSE) as $field) {
$field = str_replace("\n", "\n ", "$field");
$out .= "\n- " . $field;
}
}
if ($this->getAllExtraData()) {
$data = str_replace("\n", "\n ", print_r($this->getAllExtraData(), TRUE));
$out .= "\nExtra data: " . $data;
}
return $out;
}
}
