You are here

IndexAddFieldsForm.php in Search API 8

File

src/Form/IndexAddFieldsForm.php
View source
<?php

namespace Drupal\search_api\Form;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\Core\Url;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\Processor\ConfigurablePropertyInterface;
use Drupal\search_api\Processor\ProcessorPropertyInterface;
use Drupal\search_api\Utility\DataTypeHelperInterface;
use Drupal\search_api\Utility\FieldsHelperInterface;
use Drupal\search_api\Utility\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a form for adding fields to a search index.
 */
class IndexAddFieldsForm extends EntityForm {
  use UnsavedConfigurationFormTrait;

  /**
   * The fields helper.
   *
   * @var \Drupal\search_api\Utility\FieldsHelperInterface
   */
  protected $fieldsHelper;

  /**
   * The data type helper.
   *
   * @var \Drupal\search_api\Utility\DataTypeHelperInterface|null
   */
  protected $dataTypeHelper;

  /**
   * The index for which the fields are configured.
   *
   * @var \Drupal\search_api\IndexInterface
   */
  protected $entity;

  /**
   * The parameters of the current page request.
   *
   * @var array
   */
  protected $parameters;

  /**
   * List of types that failed to map to a Search API type.
   *
   * The unknown types are the keys and map to arrays of fields that were
   * ignored because they are of this type.
   *
   * @var string[][]
   */
  protected $unmappedFields = [];

  /**
   * The "id" attribute of the generated form.
   *
   * @var string
   */
  protected $formIdAttribute;

  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Constructs an IndexAddFieldsForm object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
   *   The fields helper.
   * @param \Drupal\search_api\Utility\DataTypeHelperInterface $data_type_helper
   *   The data type helper.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer to use.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   * @param array $parameters
   *   The parameters for this page request.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldsHelperInterface $fields_helper, DataTypeHelperInterface $data_type_helper, RendererInterface $renderer, DateFormatterInterface $date_formatter, MessengerInterface $messenger, array $parameters) {
    $this->entityTypeManager = $entity_type_manager;
    $this->fieldsHelper = $fields_helper;
    $this->dataTypeHelper = $data_type_helper;
    $this->renderer = $renderer;
    $this->dateFormatter = $date_formatter;
    $this->messenger = $messenger;
    $this->parameters = $parameters;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $entity_type_manager = $container
      ->get('entity_type.manager');
    $fields_helper = $container
      ->get('search_api.fields_helper');
    $data_type_helper = $container
      ->get('search_api.data_type_helper');
    $renderer = $container
      ->get('renderer');
    $date_formatter = $container
      ->get('date.formatter');
    $request_stack = $container
      ->get('request_stack');
    $messenger = $container
      ->get('messenger');
    $parameters = $request_stack
      ->getCurrentRequest()->query
      ->all();
    return new static($entity_type_manager, $fields_helper, $data_type_helper, $renderer, $date_formatter, $messenger, $parameters);
  }

  /**
   * {@inheritdoc}
   */
  public function getBaseFormId() {
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'search_api_index_add_fields';
  }

  /**
   * Retrieves a single page request parameter.
   *
   * @param string $name
   *   The name of the parameter.
   * @param string|null $default
   *   The value to return if the parameter isn't present.
   *
   * @return string|null
   *   The parameter value.
   */
  public function getParameter($name, $default = NULL) {
    return $this->parameters[$name] ?? $default;
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $index = $this->entity;

    // Do not allow the form to be cached. See
    // \Drupal\views_ui\ViewEditForm::form().
    $form_state
      ->disableCache();
    $this
      ->checkEntityEditable($form, $index);
    $args['%index'] = $index
      ->label();
    $form['#title'] = $this
      ->t('Add fields to index %index', $args);
    $this->formIdAttribute = Html::getUniqueId($this
      ->getFormId());
    $form['#id'] = $this->formIdAttribute;
    $form['messages'] = [
      '#type' => 'status_messages',
    ];
    $datasources = [
      '' => NULL,
    ];
    $datasources += $this->entity
      ->getDatasources();
    foreach ($datasources as $datasource_id => $datasource) {
      $item = $this
        ->getDatasourceListItem($datasource);
      if ($item) {
        $form['datasources']['datasource_' . $datasource_id] = $item;
      }
    }
    $form['actions'] = $this
      ->actionsElement($form, $form_state);

    // Log any unmapped types that were encountered.
    if ($this->unmappedFields) {
      $unmapped_fields = [];
      foreach ($this->unmappedFields as $type => $fields) {
        foreach ($fields as $field) {
          $unmapped_fields[] = new FormattableMarkup('@field (type "@type")', [
            '@field' => $field,
            '@type' => $type,
          ]);
        }
      }
      $form['unmapped_types'] = [
        '#type' => 'details',
        '#title' => $this
          ->t('Skipped fields'),
        'fields' => [
          '#theme' => 'item_list',
          '#items' => $unmapped_fields,
          '#prefix' => $this
            ->t('The following fields cannot be indexed since there is no type mapping for them:'),
          '#suffix' => $this
            ->t("If you think one of these fields should be available for indexing, please report this in the module's <a href=':url'>issue queue</a>. (Make sure to first search for an existing issue for this field.) Please note that entity-valued fields generally can be indexed by either indexing their parent reference field, or their child entity ID field.", [
            ':url' => Url::fromUri('https://www.drupal.org/project/issues/search_api')
              ->toString(),
          ]),
        ],
      ];
    }
    return $form;
  }

  /**
   * Creates a list item for one datasource.
   *
   * @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
   *   The datasource, or NULL for general properties.
   *
   * @return array
   *   A render array representing the given datasource and, possibly, its
   *   attached properties.
   */
  protected function getDatasourceListItem(DatasourceInterface $datasource = NULL) {
    $datasource_id = $datasource ? $datasource
      ->getPluginId() : NULL;
    $datasource_id_param = $datasource_id ?: '';
    $properties = $this->entity
      ->getPropertyDefinitions($datasource_id);
    if ($properties) {
      $active_property_path = '';
      $active_datasource = $this
        ->getParameter('datasource');
      if ($active_datasource !== NULL && $active_datasource == $datasource_id_param) {
        $active_property_path = $this
          ->getParameter('property_path', '');
      }
      $base_url = $this->entity
        ->toUrl('add-fields');
      $base_url
        ->setOption('query', [
        'datasource' => $datasource_id_param,
      ]);
      $item = $this
        ->getPropertiesList($properties, $active_property_path, $base_url, $datasource_id);
      $item['#title'] = $datasource ? $datasource
        ->label() : $this
        ->t('General');
      return $item;
    }
    return NULL;
  }

  /**
   * 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|null $datasource_id
   *   The datasource ID of the listed properties, or NULL for
   *   datasource-independent properties.
   * @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, $datasource_id, $parent_path = '', $label_prefix = '') {
    $list = [];
    $active_item = '';
    if ($active_property_path) {
      list($active_item, $active_property_path) = explode(':', $active_property_path, 2) + [
        1 => '',
      ];
    }
    $type_mapping = $this->dataTypeHelper
      ->getFieldTypeMapping();
    $query_base = $base_url
      ->getOption('query');
    foreach ($properties as $key => $property) {
      if ($property instanceof ProcessorPropertyInterface && $property
        ->isHidden()) {
        continue;
      }
      $this_path = $parent_path ? $parent_path . ':' : '';
      $this_path .= $key;
      $label = $property
        ->getLabel();
      $property = $this->fieldsHelper
        ->getInnerProperty($property);
      $can_be_indexed = TRUE;
      $nested_properties = [];
      $parent_child_type = NULL;
      if ($property instanceof ComplexDataDefinitionInterface) {
        $can_be_indexed = FALSE;
        $nested_properties = $this->fieldsHelper
          ->getNestedProperties($property);
        $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 (isset($nested_properties['entity'])) {
          $entity_property = $nested_properties['entity'];
          if ($entity_property instanceof DataReferenceDefinitionInterface) {
            $target = $entity_property
              ->getTargetDefinition();
            if ($target instanceof EntityDataDefinitionInterface) {
              if (!$this->fieldsHelper
                ->isContentEntityType($target
                ->getEntityTypeId())) {
                unset($nested_properties['entity']);
              }
            }
          }
        }

        // Remove hidden properties right away so we don't even show a "+" link
        // in case all sub-properties are hidden.
        foreach ($nested_properties as $nested_key => $nested_property) {
          if ($nested_property instanceof ProcessorPropertyInterface && $nested_property
            ->isHidden()) {
            unset($nested_properties[$nested_key]);
          }
        }
      }

      // Don't allow indexing of properties with unmapped types. Also, prefer
      // a "parent.child" type mapping (taking into account the parent property
      // for, for example, 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;
      }
      $item = [
        '#type' => 'container',
        '#attributes' => [
          'class' => [
            'container-inline',
          ],
        ],
      ];
      $nested_list = [];
      if ($nested_properties) {
        if ($key == $active_item) {
          $link_url = clone $base_url;
          $query_base['property_path'] = $parent_path;
          $link_url
            ->setOption('query', $query_base);
          $item['expand_link'] = [
            '#type' => 'link',
            '#title' => '(-) ',
            '#attributes' => [
              'data-disable-refocus' => [
                'true',
              ],
            ],
            '#url' => $link_url,
            '#ajax' => [
              'wrapper' => $this->formIdAttribute,
            ],
          ];
          $nested_list = $this
            ->getPropertiesList($nested_properties, $active_property_path, $base_url, $datasource_id, $this_path, $label_prefix . $label . ' » ');
        }
        else {
          $link_url = clone $base_url;
          $query_base['property_path'] = $this_path;
          $link_url
            ->setOption('query', $query_base);
          $item['expand_link'] = [
            '#type' => 'link',
            '#title' => '(+) ',
            '#attributes' => [
              'data-disable-refocus' => [
                'true',
              ],
            ],
            '#url' => $link_url,
            '#ajax' => [
              'wrapper' => $this->formIdAttribute,
            ],
          ];
        }
      }
      $label_markup = Html::escape($label);
      $escaped_path = Html::escape($this_path);
      $label_markup = "{$label_markup} <small>(<code>{$escaped_path}</code>)</small>";
      $item['label']['#markup'] = $label_markup . ' ';
      if ($can_be_indexed) {
        $item['add'] = [
          '#type' => 'submit',
          '#name' => Utility::createCombinedId($datasource_id, $this_path),
          '#value' => $this
            ->t('Add'),
          '#submit' => [
            '::addField',
            '::save',
          ],
          '#attributes' => [
            'data-disable-refocus' => [
              'true',
            ],
          ],
          '#property' => $property,
          '#prefixed_label' => $label_prefix . $label,
          '#data_type' => $type_mapping[$type],
          '#ajax' => [
            'wrapper' => $this->formIdAttribute,
          ],
        ];
      }
      if ($nested_list) {
        $item['properties'] = $nested_list;
      }
      $list[$key] = $item;
    }
    if ($list) {
      uasort($list, [
        static::class,
        'compareFieldLabels',
      ]);
      $list['#theme'] = 'search_api_form_item_list';
    }
    return $list;
  }

  /**
   * Compares labels of property render arrays.
   *
   * Used as an uasort() callback in
   * \Drupal\search_api\Form\IndexAddFieldsForm::getPropertiesList().
   *
   * @param array $a
   *   First property render array.
   * @param array $b
   *   Second property render array.
   *
   * @return int
   *   -1, 0 or 1 if the first array should be considered, respectively, less
   *   than, equal to or greater than the second.
   */
  public static function compareFieldLabels(array $a, array $b) {
    $a_title = (string) $a['label']['#markup'];
    $b_title = (string) $b['label']['#markup'];
    return strnatcasecmp($a_title, $b_title);
  }

  /**
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    return [
      'done' => [
        '#type' => 'link',
        '#title' => $this
          ->t('Done'),
        '#url' => $this->entity
          ->toUrl('fields'),
        '#attributes' => [
          'class' => [
            'button',
          ],
        ],
      ],
    ];
  }

  /**
   * 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 = $this->fieldsHelper
      ->createFieldFromProperty($this->entity, $property, $datasource_id, $property_path, NULL, $button['#data_type']);
    $field
      ->setLabel($button['#prefixed_label']);
    $this->entity
      ->addField($field);
    if ($property instanceof ConfigurablePropertyInterface) {
      $parameters = [
        'search_api_index' => $this->entity
          ->id(),
        'field_id' => $field
          ->getFieldIdentifier(),
      ];
      $options = [];
      $route = $this
        ->getRequest()->attributes
        ->get('_route');
      if ($route === 'entity.search_api_index.add_fields_ajax') {
        $options['query'] = [
          MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
          'modal_redirect' => 1,
        ];
      }
      $form_state
        ->setRedirect('entity.search_api_index.field_config', $parameters, $options);
    }
    $args['%label'] = $field
      ->getLabel();
    $this->messenger
      ->addStatus($this
      ->t('Field %label was added to the index.', $args));
  }

}

Classes

Namesort descending Description
IndexAddFieldsForm Provides a form for adding fields to a search index.