You are here

class FieldsHelper in Search API 8

Provides helper methods for dealing with Search API fields and properties.

Hierarchy

Expanded class hierarchy of FieldsHelper

3 files declare their use of FieldsHelper
BackendPluginBase.php in src/Backend/BackendPluginBase.php
FieldsHelperTest.php in tests/src/Unit/FieldsHelperTest.php
TestItemsTrait.php in tests/src/Unit/Processor/TestItemsTrait.php
1 string reference to 'FieldsHelper'
search_api.services.yml in ./search_api.services.yml
search_api.services.yml
1 service uses FieldsHelper
search_api.fields_helper in ./search_api.services.yml
Drupal\search_api\Utility\FieldsHelper

File

src/Utility/FieldsHelper.php, line 35

Namespace

Drupal\search_api\Utility
View source
class FieldsHelper implements FieldsHelperInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The entity type bundle info service.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $entityBundleInfo;

  /**
   * The data type plugin manager.
   *
   * @var \Drupal\search_api\Utility\DataTypeHelperInterface
   */
  protected $dataTypeHelper;

  /**
   * Cache for the field type mapping.
   *
   * @var array|null
   *
   * @see getFieldTypeMapping()
   */
  protected $fieldTypeMapping;

  /**
   * Cache for the fallback data type mapping per index.
   *
   * @var array
   *
   * @see getDataTypeFallbackMapping()
   */
  protected $dataTypeFallbackMapping = [];

  /**
   * Constructs a FieldsHelper object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
   *   The entity field manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityBundleInfo
   *   The entity type bundle info service.
   * @param \Drupal\search_api\Utility\DataTypeHelperInterface $dataTypeHelper
   *   The data type helper service.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, EntityFieldManagerInterface $entityFieldManager, EntityTypeBundleInfoInterface $entityBundleInfo, DataTypeHelperInterface $dataTypeHelper) {
    $this->entityTypeManager = $entityTypeManager;
    $this->entityFieldManager = $entityFieldManager;
    $this->entityBundleInfo = $entityBundleInfo;
    $this->dataTypeHelper = $dataTypeHelper;
  }

  /**
   * {@inheritdoc}
   */
  public function extractFields(ComplexDataInterface $item, array $fields, $langcode = NULL) {

    // If a language code was given, get the correct translation (if possible).
    if ($langcode) {
      if ($item instanceof TranslatableInterface) {
        if ($item
          ->hasTranslation($langcode)) {
          $item = $item
            ->getTranslation($langcode);
        }
      }
      else {
        $value = $item
          ->getValue();
        if ($value instanceof ContentEntityInterface) {
          if ($value
            ->hasTranslation($langcode)) {
            $item = $value
              ->getTranslation($langcode)
              ->getTypedData();
          }
        }
      }
    }

    // Figure out which fields are directly on the item and which need to be
    // extracted from nested items.
    $directFields = [];
    $nestedFields = [];
    foreach (array_keys($fields) as $key) {
      if (strpos($key, ':') !== FALSE) {
        list($direct, $nested) = explode(':', $key, 2);
        $nestedFields[$direct][$nested] = $fields[$key];
      }
      else {
        $directFields[] = $key;
      }
    }

    // Extract the direct fields.
    $properties = $item
      ->getProperties(TRUE);
    foreach ($directFields as $key) {
      if (empty($properties[$key])) {
        continue;
      }
      $data = $item
        ->get($key);
      foreach ($fields[$key] as $field) {
        $this
          ->extractField($data, $field);
      }
    }

    // Recurse for all nested fields.
    foreach ($nestedFields as $direct => $fieldsNested) {
      if (empty($properties[$direct])) {
        continue;
      }
      $itemNested = $item
        ->get($direct);
      if ($itemNested instanceof DataReferenceInterface) {
        $itemNested = $itemNested
          ->getTarget();
      }
      if ($itemNested instanceof EntityInterface) {
        $itemNested = $itemNested
          ->getTypedData();
      }
      if ($itemNested instanceof ComplexDataInterface && !$itemNested
        ->isEmpty()) {
        $this
          ->extractFields($itemNested, $fieldsNested, $langcode);
      }
      elseif ($itemNested instanceof ListInterface && !$itemNested
        ->isEmpty()) {
        foreach ($itemNested as $listItem) {
          if ($listItem instanceof ComplexDataInterface && !$listItem
            ->isEmpty()) {
            $this
              ->extractFields($listItem, $fieldsNested, $langcode);
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function extractField(TypedDataInterface $data, FieldInterface $field) {
    $values = $this
      ->extractFieldValues($data);
    foreach ($values as $i => $value) {
      $field
        ->addValue($value);
    }
    $field
      ->setOriginalType($data
      ->getDataDefinition()
      ->getDataType());
  }

  /**
   * {@inheritdoc}
   */
  public function extractFieldValues(TypedDataInterface $data) {
    $definition = $data
      ->getDataDefinition();

    // Process list data types.
    if ($definition
      ->isList()) {
      $values = [];
      foreach ($data as $piece) {
        $values[] = $this
          ->extractFieldValues($piece);
      }
      return $values ? call_user_func_array('array_merge', $values) : [];
    }

    // Process complex data types.
    if ($definition instanceof ComplexDataDefinitionInterface) {
      $main_property_name = $definition
        ->getMainPropertyName();
      $data_properties = $data
        ->getProperties(TRUE);
      if (isset($data_properties[$main_property_name])) {
        return $this
          ->extractFieldValues($data_properties[$main_property_name]);
      }
      return [];
    }

    // Process simple (scalar) data types.
    $value = $data
      ->getValue();
    if (is_array($value)) {
      return array_values($value);
    }
    return [
      $value,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function extractItemValues(array $items, array $required_properties, $load = TRUE) {
    $extracted_values = [];

    /** @var \Drupal\search_api\Item\ItemInterface $item */
    foreach ($items as $i => $item) {
      $index = $item
        ->getIndex();
      $item_values = [];

      /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
      $missing_fields = [];
      $processor_fields = [];
      $needed_processors = [];
      foreach ([
        NULL,
        $item
          ->getDatasourceId(),
      ] as $datasource_id) {
        if (empty($required_properties[$datasource_id])) {
          continue;
        }
        $properties = $index
          ->getPropertyDefinitions($datasource_id);
        foreach ($required_properties[$datasource_id] as $property_path => $combined_id) {
          $item_values[$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), $datasource_id, $property_path) as $field) {
            $item_values[$combined_id] = $field
              ->getValues();
            continue 2;
          }

          // There are no values present on the item for this property. If we
          // don't want to extract any fields, skip it.
          if (!$load) {
            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.
          // In case the first part of the property path refers to a
          // processor-defined property, we need to use the processor to
          // retrieve the value. Otherwise, we extract it normally from the
          // data object.
          $property_name = Utility::splitPropertyPath($property_path, FALSE)[0];
          $property = $properties[$property_name] ?? NULL;
          if ($property instanceof ProcessorPropertyInterface) {
            $field_info = [
              'datasource_id' => $datasource_id,
              'property_path' => $property_path,
            ];
            if ($property instanceof ConfigurablePropertyInterface) {
              $field_info['configuration'] = $property
                ->defaultConfiguration();

              // If the index contains a field with that property, just use the
              // configuration from there instead of the default configuration.
              // This will probably be what users expect in most situations.
              foreach ($this
                ->filterForPropertyPath($index
                ->getFields(), $datasource_id, $property_path) as $field) {
                $field_info['configuration'] = $field
                  ->getConfiguration();
                break;
              }
            }
            $processor_fields[] = $this
              ->createField($index, $combined_id, $field_info);
            $needed_processors[$property
              ->getProcessorId()] = TRUE;
          }
          elseif ($datasource_id) {
            $missing_fields[$property_path][] = $this
              ->createField($index, $combined_id);
          }
        }
      }
      if ($missing_fields) {
        $this
          ->extractFields($item
          ->getOriginalObject(), $missing_fields);
        foreach ($missing_fields as $property_fields) {
          foreach ($property_fields as $field) {
            $item_values[$field
              ->getFieldIdentifier()] = $field
              ->getValues();
          }
        }
      }
      if ($processor_fields) {
        $dummy_item = clone $item;
        $dummy_item
          ->setFields($processor_fields);
        $dummy_item
          ->setFieldsExtracted(TRUE);
        $processors = $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) {
          $item_values[$field
            ->getFieldIdentifier()] = $field
            ->getValues();
        }
      }
      $extracted_values[$i] = $item_values;
    }
    return $extracted_values;
  }

  /**
   * {@inheritdoc}
   */
  public function filterForPropertyPath(array $fields, $datasource_id, $property_path) {
    $found_fields = [];
    foreach ($fields as $field_id => $field) {
      if ($field
        ->getDatasourceId() === $datasource_id && $field
        ->getPropertyPath() === $property_path) {
        $found_fields[$field_id] = $field;
      }
    }
    return $found_fields;
  }

  /**
   * {@inheritdoc}
   */
  public function getNestedProperties(ComplexDataDefinitionInterface $property) {
    $nestedProperties = $property
      ->getPropertyDefinitions();
    if ($property instanceof EntityDataDefinitionInterface) {
      $entity_type_id = $property
        ->getEntityTypeId();
      $is_content_type = $this
        ->isContentEntityType($entity_type_id);
      if ($is_content_type) {
        $bundles = $property
          ->getBundles() ?: array_keys($this->entityBundleInfo
          ->getBundleInfo($entity_type_id));
        foreach ($bundles as $bundle) {
          $bundleProperties = $this->entityFieldManager
            ->getFieldDefinitions($entity_type_id, $bundle);
          $nestedProperties += $bundleProperties;
        }
      }
    }
    return $nestedProperties;
  }

  /**
   * {@inheritdoc}
   */
  public function retrieveNestedProperty(array $properties, $propertyPath) {
    list($key, $nestedPath) = Utility::splitPropertyPath($propertyPath, FALSE);
    if (!isset($properties[$key])) {
      return NULL;
    }
    $property = $this
      ->getInnerProperty($properties[$key]);
    if ($nestedPath === NULL) {
      return $property;
    }
    if (!$property instanceof ComplexDataDefinitionInterface) {
      return NULL;
    }
    return $this
      ->retrieveNestedProperty($this
      ->getNestedProperties($property), $nestedPath);
  }

  /**
   * {@inheritdoc}
   */
  public function getInnerProperty(DataDefinitionInterface $property) {
    while ($property instanceof ListDataDefinitionInterface) {
      $property = $property
        ->getItemDefinition();
    }
    while ($property instanceof DataReferenceDefinitionInterface) {
      $property = $property
        ->getTargetDefinition();
    }
    return $property;
  }

  /**
   * {@inheritdoc}
   */
  public function isContentEntityType($entity_type_id) {
    try {
      $definition = $this->entityTypeManager
        ->getDefinition($entity_type_id);
      return $definition
        ->entityClassImplements(ContentEntityInterface::class);
    } catch (PluginNotFoundException $e) {
      return FALSE;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function isFieldIdReserved($fieldId) {
    return substr($fieldId, 0, 11) == 'search_api_';
  }

  /**
   * {@inheritdoc}
   */
  public function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
    return new Item($index, $id, $datasource);
  }

  /**
   * {@inheritdoc}
   */
  public function createItemFromObject(IndexInterface $index, ComplexDataInterface $originalObject, $id = NULL, DatasourceInterface $datasource = NULL) {
    if (!isset($id)) {
      if (!isset($datasource)) {
        throw new \InvalidArgumentException('Need either an item ID or the datasource to create a search item from an object.');
      }
      $id = Utility::createCombinedId($datasource
        ->getPluginId(), $datasource
        ->getItemId($originalObject));
    }
    $item = $this
      ->createItem($index, $id, $datasource);
    $item
      ->setOriginalObject($originalObject);
    return $item;
  }

  /**
   * {@inheritdoc}
   */
  public function createField(IndexInterface $index, $fieldIdentifier, array $fieldInfo = []) {
    $field = new Field($index, $fieldIdentifier);
    foreach ($fieldInfo as $key => $value) {
      $method = 'set' . Container::camelize($key);
      if (method_exists($field, $method)) {
        $field
          ->{$method}($value);
      }
    }
    return $field;
  }

  /**
   * {@inheritdoc}
   */
  public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasourceId, $propertyPath, $fieldId = NULL, $type = NULL) {
    $fieldId = $fieldId ?? $this
      ->getNewFieldId($index, $propertyPath);
    if (!isset($type)) {
      $typeMapping = $this->dataTypeHelper
        ->getFieldTypeMapping();
      $propertyType = $property
        ->getDataType();
      if (isset($typeMapping[$propertyType])) {
        $type = $typeMapping[$propertyType];
      }
      else {
        $propertyName = $property
          ->getLabel();
        throw new SearchApiException("No default data type mapping could be found for property '{$propertyName}' ({$propertyPath}) of type '{$propertyType}'.");
      }
    }
    $fieldInfo = [
      'label' => $property
        ->getLabel(),
      'datasource_id' => $datasourceId,
      'property_path' => $propertyPath,
      'type' => $type,
      'data_definition' => $property,
    ];
    if ($property instanceof ConfigurablePropertyInterface) {
      $fieldInfo['configuration'] = $property
        ->defaultConfiguration();
    }
    return $this
      ->createField($index, $fieldId, $fieldInfo);
  }

  /**
   * {@inheritdoc}
   */
  public function getNewFieldId(IndexInterface $index, $propertyPath) {
    list(, $suggestedId) = Utility::splitPropertyPath($propertyPath);

    // Avoid clashes with reserved IDs by removing the reserved "search_api_"
    // from our suggested ID.
    $suggestedId = str_replace('search_api_', '', $suggestedId);
    $fieldId = $suggestedId;
    $i = 0;
    while ($index
      ->getField($fieldId)) {
      $fieldId = $suggestedId . '_' . ++$i;
    }
    while ($this
      ->isFieldIdReserved($fieldId)) {
      $fieldId = '_' . $fieldId;
    }
    return $fieldId;
  }

  /**
   * {@inheritdoc}
   */
  public function compareFieldLabels(FieldInterface $a, FieldInterface $b) {
    return strnatcasecmp($a
      ->getLabel(), $b
      ->getLabel());
  }

}

Members

Namesort descending Modifiers Type Description Overrides
FieldsHelper::$dataTypeFallbackMapping protected property Cache for the fallback data type mapping per index.
FieldsHelper::$dataTypeHelper protected property The data type plugin manager.
FieldsHelper::$entityBundleInfo protected property The entity type bundle info service.
FieldsHelper::$entityFieldManager protected property The entity field manager.
FieldsHelper::$entityTypeManager protected property The entity type manager.
FieldsHelper::$fieldTypeMapping protected property Cache for the field type mapping.
FieldsHelper::compareFieldLabels public function Compares two fields for alphabetic sorting according to their labels. Overrides FieldsHelperInterface::compareFieldLabels
FieldsHelper::createField public function Creates a new field object wrapping a field of the given index. Overrides FieldsHelperInterface::createField
FieldsHelper::createFieldFromProperty public function Creates a new field on an index based on a property. Overrides FieldsHelperInterface::createFieldFromProperty
FieldsHelper::createItem public function Creates a search item object. Overrides FieldsHelperInterface::createItem
FieldsHelper::createItemFromObject public function Creates a search item object by wrapping an existing complex data object. Overrides FieldsHelperInterface::createItemFromObject
FieldsHelper::extractField public function Extracts value and original type from a single piece of data. Overrides FieldsHelperInterface::extractField
FieldsHelper::extractFields public function Extracts specific field values from a complex data object. Overrides FieldsHelperInterface::extractFields
FieldsHelper::extractFieldValues public function Extracts field values from a typed data object. Overrides FieldsHelperInterface::extractFieldValues
FieldsHelper::extractItemValues public function Extracts property values from items. Overrides FieldsHelperInterface::extractItemValues
FieldsHelper::filterForPropertyPath public function Filters the given fields for those with the specified property path. Overrides FieldsHelperInterface::filterForPropertyPath
FieldsHelper::getInnerProperty public function Retrieves the inner property definition of a compound property definition. Overrides FieldsHelperInterface::getInnerProperty
FieldsHelper::getNestedProperties public function Retrieves a list of nested properties from a complex property. Overrides FieldsHelperInterface::getNestedProperties
FieldsHelper::getNewFieldId public function Finds a new unique field identifier on the given index. Overrides FieldsHelperInterface::getNewFieldId
FieldsHelper::isContentEntityType public function Checks whether the given entity type is a content entity type. Overrides FieldsHelperInterface::isContentEntityType
FieldsHelper::isFieldIdReserved public function Determines whether a field ID is reserved for special use. Overrides FieldsHelperInterface::isFieldIdReserved
FieldsHelper::retrieveNestedProperty public function Retrieves a nested property from a list of properties. Overrides FieldsHelperInterface::retrieveNestedProperty
FieldsHelper::__construct public function Constructs a FieldsHelper object.