/** * Tests processing stages. */ public function testGetProcessingStages() { $namespaces = new ArrayObject(); $sut = new ProcessorPluginManager($namespaces, $this->cache, $this->moduleHandler, $this->translator); $stages = [ProcessorInterface::STAGE_PRE_QUERY, ProcessorInterface::STAGE_POST_QUERY, ProcessorInterface::STAGE_BUILD]; $this->assertEquals($stages, array_keys($sut->getProcessingStages())); }
/** * Tests plugin manager's getDefinitions method. */ public function testGetDefinitions() { $definitions = array('foo' => array('label' => $this->randomMachineName())); $this->discovery->expects($this->once())->method('getDefinitions')->willReturn($definitions); $this->assertSame($definitions, $this->sut->getDefinitions()); }
/** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { $form['#attached']['library'][] = 'facets/drupal.facets.admin_css'; /** @var \Drupal\facets\FacetInterface $facet */ $facet = $this->entity; $widget_options = []; foreach ($this->getWidgetPluginManager()->getDefinitions() as $widget_id => $definition) { $widget_options[$widget_id] = !empty($definition['label']) ? $definition['label'] : $widget_id; } $form['widget'] = ['#type' => 'radios', '#title' => $this->t('Widget'), '#description' => $this->t('The widget used for displaying this facet.'), '#options' => $widget_options, '#default_value' => $facet->getWidget(), '#required' => TRUE, '#ajax' => ['trigger_as' => ['name' => 'widget_configure'], 'callback' => '::buildAjaxWidgetConfigForm', 'wrapper' => 'facets-widget-config-form', 'method' => 'replace', 'effect' => 'fade']]; $form['widget_configs'] = ['#type' => 'container', '#attributes' => ['id' => 'facets-widget-config-form'], '#tree' => TRUE]; $form['widget_configure_button'] = ['#type' => 'submit', '#name' => 'widget_configure', '#value' => $this->t('Configure widget'), '#limit_validation_errors' => [['widget']], '#submit' => ['::submitAjaxWidgetConfigForm'], '#ajax' => ['callback' => '::buildAjaxWidgetConfigForm', 'wrapper' => 'facets-widget-config-form'], '#attributes' => ['class' => ['js-hide']]]; $this->buildWidgetConfigForm($form, $form_state); // Retrieve lists of all processors, and the stages and weights they have. if (!$form_state->has('processors')) { $all_processors = $facet->getProcessors(FALSE); $sort_processors = function (ProcessorInterface $a, ProcessorInterface $b) { return strnatcasecmp((string) $a->getPluginDefinition()['label'], (string) $b->getPluginDefinition()['label']); }; uasort($all_processors, $sort_processors); } else { $all_processors = $form_state->get('processors'); } $enabled_processors = $facet->getProcessors(TRUE); $stages = $this->processorPluginManager->getProcessingStages(); $processors_by_stage = array(); foreach ($stages as $stage => $definition) { $processors_by_stage[$stage] = $facet->getProcessorsByStage($stage, FALSE); } $form['#tree'] = TRUE; $form['#attached']['library'][] = 'facets/drupal.facets.index-active-formatters'; $form['#title'] = $this->t('Edit %label facet', array('%label' => $facet->label())); // Add the list of all other processors with checkboxes to enable/disable // them. $form['facet_settings'] = array('#type' => 'fieldset', '#title' => $this->t('Facet settings'), '#attributes' => array('class' => array('search-api-status-wrapper'))); foreach ($all_processors as $processor_id => $processor) { if (!$processor instanceof WidgetOrderProcessorInterface && !$processor instanceof UrlProcessorInterface) { $clean_css_id = Html::cleanCssIdentifier($processor_id); $form['facet_settings'][$processor_id]['status'] = array('#type' => 'checkbox', '#title' => (string) $processor->getPluginDefinition()['label'], '#default_value' => $processor->isLocked() || !empty($enabled_processors[$processor_id]), '#description' => $processor->getDescription(), '#attributes' => array('class' => array('search-api-processor-status-' . $clean_css_id), 'data-id' => $clean_css_id), '#disabled' => $processor->isLocked(), '#access' => !$processor->isHidden()); $processor_form_state = new SubFormState($form_state, ['facet_settings', $processor_id, 'settings']); $processor_form = $processor->buildConfigurationForm($form, $processor_form_state, $facet); if ($processor_form) { $form['facet_settings'][$processor_id]['settings'] = array('#type' => 'details', '#title' => $this->t('%processor settings', ['%processor' => (string) $processor->getPluginDefinition()['label']]), '#open' => TRUE, '#attributes' => array('class' => array('facets-processor-settings-' . Html::cleanCssIdentifier($processor_id), 'facets-processor-settings-facet', 'facets-processor-settings')), '#states' => array('visible' => array(':input[name="facet_settings[' . $processor_id . '][status]"]' => array('checked' => TRUE)))); $form['facet_settings'][$processor_id]['settings'] += $processor_form; } } } // Add the list of widget sort processors with checkboxes to enable/disable // them. $form['facet_sorting'] = array('#type' => 'fieldset', '#title' => $this->t('Facet sorting'), '#attributes' => array('class' => array('search-api-status-wrapper'))); foreach ($all_processors as $processor_id => $processor) { if ($processor instanceof WidgetOrderProcessorInterface) { $clean_css_id = Html::cleanCssIdentifier($processor_id); $form['facet_sorting'][$processor_id]['status'] = array('#type' => 'checkbox', '#title' => (string) $processor->getPluginDefinition()['label'], '#default_value' => $processor->isLocked() || !empty($enabled_processors[$processor_id]), '#description' => $processor->getDescription(), '#attributes' => array('class' => array('search-api-processor-status-' . $clean_css_id), 'data-id' => $clean_css_id), '#disabled' => $processor->isLocked(), '#access' => !$processor->isHidden()); $processor_form_state = new SubFormState($form_state, array('facet_sorting', $processor_id, 'settings')); $processor_form = $processor->buildConfigurationForm($form, $processor_form_state, $facet); if ($processor_form) { $form['facet_sorting'][$processor_id]['settings'] = array('#type' => 'container', '#open' => TRUE, '#attributes' => array('class' => array('facets-processor-settings-' . Html::cleanCssIdentifier($processor_id), 'facets-processor-settings-sorting', 'facets-processor-settings')), '#states' => array('visible' => array(':input[name="facet_sorting[' . $processor_id . '][status]"]' => array('checked' => TRUE)))); $form['facet_sorting'][$processor_id]['settings'] += $processor_form; } } } $form['facet_settings']['only_visible_when_facet_source_is_visible'] = ['#type' => 'checkbox', '#title' => $this->t('Hide facet when facet source is not rendered'), '#description' => $this->t('When checked, this facet will only be rendered when the facet source is rendered. If you want to show facets on other pages too, you need to uncheck this setting.'), '#default_value' => $facet->getOnlyVisibleWhenFacetSourceIsVisible()]; $form['facet_settings']['show_only_one_result'] = ['#type' => 'checkbox', '#title' => $this->t('Make sure only one result can be shown.'), '#description' => $this->t('When checked, this will make sure that only one result can be selected for this facet at one time.'), '#default_value' => $facet->getShowOnlyOneResult()]; $form['facet_settings']['url_alias'] = ['#type' => 'machine_name', '#title' => $this->t('Url alias'), '#default_value' => $facet->getUrlAlias(), '#maxlength' => 50, '#required' => TRUE, '#machine_name' => ['exists' => [\Drupal::service('entity_type.manager')->getStorage('facets_facet'), 'load'], 'source' => ['name']]]; $empty_behavior_config = $facet->getEmptyBehavior(); $form['facet_settings']['empty_behavior'] = ['#type' => 'radios', '#title' => t('Empty facet behavior'), '#default_value' => $empty_behavior_config['behavior'] ?: 'none', '#options' => ['none' => t('Do not display facet'), 'text' => t('Display text')], '#description' => $this->t('The action to take when a facet has no items.'), '#required' => TRUE]; $form['facet_settings']['empty_behavior_container'] = ['#type' => 'container', '#states' => array('visible' => array(':input[name="facet_settings[empty_behavior]"]' => array('value' => 'text')))]; $form['facet_settings']['empty_behavior_container']['empty_behavior_text'] = ['#type' => 'text_format', '#title' => $this->t('Empty text'), '#format' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text_format'] : 'plain_text', '#editor' => TRUE, '#default_value' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text'] : '']; $form['facet_settings']['query_operator'] = ['#type' => 'radios', '#title' => $this->t('Operator'), '#options' => ['OR' => $this->t('OR'), 'AND' => $this->t('AND')], '#description' => $this->t('AND filters are exclusive and narrow the result set. OR filters are inclusive and widen the result set.'), '#default_value' => $facet->getQueryOperator()]; $form['facet_settings']['exclude'] = ['#type' => 'checkbox', '#title' => $this->t('Exclude'), '#description' => $this->t('Make the search exclude selected facets, instead of restricting it to them.'), '#default_value' => $facet->getExclude()]; $form['facet_settings']['weight'] = ['#type' => 'number', '#title' => $this->t('Weight'), '#default_value' => $facet->getWeight(), '#maxlength' => 4, '#required' => TRUE]; $form['weights'] = array('#type' => 'details', '#title' => t('Advanced settings'), '#collapsible' => TRUE, '#collapsed' => TRUE); $form['weights']['order'] = ['#markup' => "<h3>" . t('Processor order') . "</h3>"]; // Order enabled processors per stage, create all the containers for the // different stages. foreach ($stages as $stage => $description) { $form['weights'][$stage] = array('#type' => 'fieldset', '#title' => $description['label'], '#attributes' => array('class' => array('search-api-stage-wrapper', 'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage)))); $form['weights'][$stage]['order'] = array('#type' => 'table'); $form['weights'][$stage]['order']['#tabledrag'][] = array('action' => 'order', 'relationship' => 'sibling', 'group' => 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage)); } $processor_settings = $facet->getProcessorConfigs(); // Fill in the containers previously created with the processors that are // enabled on the facet. foreach ($processors_by_stage as $stage => $processors) { /** @var \Drupal\facets\Processor\ProcessorInterface $processor */ foreach ($processors as $processor_id => $processor) { $weight = isset($processor_settings[$processor_id]['weights'][$stage]) ? $processor_settings[$processor_id]['weights'][$stage] : $processor->getDefaultWeight($stage); if ($processor->isHidden()) { $form['processors'][$processor_id]['weights'][$stage] = array('#type' => 'value', '#value' => $weight); continue; } $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'draggable'; $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'search-api-processor-weight--' . Html::cleanCssIdentifier($processor_id); $form['weights'][$stage]['order'][$processor_id]['#weight'] = $weight; $form['weights'][$stage]['order'][$processor_id]['label']['#plain_text'] = (string) $processor->getPluginDefinition()['label']; $form['weights'][$stage]['order'][$processor_id]['weight'] = array('#type' => 'weight', '#title' => $this->t('Weight for processor %title', array('%title' => (string) $processor->getPluginDefinition()['label'])), '#title_display' => 'invisible', '#default_value' => $weight, '#parents' => array('processors', $processor_id, 'weights', $stage), '#attributes' => array('class' => array('search-api-processor-weight-' . Html::cleanCssIdentifier($stage)))); } } // Add vertical tabs containing the settings for the processors. Tabs for // disabled processors are hidden with JS magic, but need to be included in // case the processor is enabled. $form['processor_settings'] = array('#title' => $this->t('Processor settings'), '#type' => 'vertical_tabs'); return $form; }
/** * Builds a facet and returns it as a renderable array. * * This method delegates to the relevant plugins to render a facet, it calls * out to a widget plugin to do the actual rendering when results are found. * When no results are found it calls out to the correct empty result plugin * to build a render array. * * Before doing any rendering, the processors that implement the * BuildProcessorInterface enabled on this facet will run. * * @param \Drupal\facets\FacetInterface $facet * The facet we should build. * * @return array * Facet render arrays. * * @throws \Drupal\facets\Exception\InvalidProcessorException * Throws an exception when an invalid processor is linked to the facet. */ public function build(FacetInterface $facet) { // It might be that the facet received here, is not the same as the already // loaded facets in the FacetManager. // For that reason, get the facet from the already loaded facets in the // FacetManager. $facet = $this->facets[$facet->id()]; $facet_source_id = $facet->getFacetSourceId(); if ($facet->getOnlyVisibleWhenFacetSourceIsVisible()) { // Block rendering and processing should be stopped when the facet source // is not available on the page. Returning an empty array here is enough // to halt all further processing. $facet_source = $facet->getFacetSource(); if (!$facet_source->isRenderedInCurrentRequest()) { return []; } } // For clarity, process facets is called each build. // The first facet therefor will trigger the processing. Note that // processing is done only once, so repeatedly calling this method will not // trigger the processing more than once. $this->processFacets($facet_source_id); // Get the current results from the facets and let all processors that // trigger on the build step do their build processing. // @see \Drupal\facets\Processor\BuildProcessorInterface. // @see \Drupal\facets\Processor\WidgetOrderProcessorInterface. $results = $facet->getResults(); $active_sorts = []; // Load all processors, because getProcessorsByStage does not return the // correct configuration for the processors. // @todo: Fix when https://www.drupal.org/node/2722267 is fixed. $processors = $facet->getProcessors(); foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) { /** @var \Drupal\facets\Processor\BuildProcessorInterface $build_processor */ $build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]); if ($build_processor instanceof WidgetOrderProcessorInterface) { // Sorting is handled last and together, to support nested sorts. $active_sorts[] = $processors[$build_processor->getPluginId()]; } else { if (!$build_processor instanceof BuildProcessorInterface) { throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface"); } $results = $build_processor->build($facet, $results); } } uasort($results, function ($a, $b) use($active_sorts) { $return = 0; foreach ($active_sorts as $sort) { if ($return = $sort->sortResults($a, $b)) { if ($sort->getConfiguration()['sort'] == 'DESC') { $return *= -1; } break; } } return $return; }); $facet->setResults($results); // No results behavior handling. Return a custom text or false depending on // settings. if (empty($facet->getResults())) { $empty_behavior = $facet->getEmptyBehavior(); if ($empty_behavior['behavior'] == 'text') { return [['#markup' => $empty_behavior['text']]]; } else { return []; } } // Let the widget plugin render the facet. /** @var \Drupal\facets\Widget\WidgetInterface $widget */ $widget = $this->widgetPluginManager->createInstance($facet->getWidget()); return [$widget->build($facet)]; }