search_api-8.x-1.15/src/Form/IndexAddFieldsForm.php
src/Form/IndexAddFieldsForm.php
<?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 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 isset($this->parameters[$name]) ? $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']); } } } } } // 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)); } }