diff --git a/search_api.api.php b/search_api.api.php index 87e25ce..b2a8c63 100644 --- a/search_api.api.php +++ b/search_api.api.php @@ -98,6 +98,34 @@ function hook_search_api_field_type_mapping_alter(array &$mapping) { } /** + * Alter the mapping of Search API data types to their default Views handlers. + * + * @param array $mapping + * An associative array with data types as the keys and Views field data + * definitions as the values. In addition to all normally defined data types, + * keys can also be "options" for any field with an options list, "entity" for + * general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE" + * being the machine name of an entity type) for entities of that type. + */ +function search_api_views_handler_mapping_alter(array &$mapping) { + $mapping['entity:my_entity_type'] = array( + 'argument' => array( + 'id' => 'my_entity_type', + ), + 'field' => array( + 'id' => 'my_entity_type', + ), + 'filter' => array( + 'id' => 'my_entity_type', + ), + 'sort' => array( + 'id' => 'my_entity_type', + ), + ); + $mapping['date']['filter']['id'] = 'my_date_filter'; +} + +/** * Allows you to log or alter the items that are indexed. * * Please be aware that generally preventing the indexing of certain items is diff --git a/search_api.views.inc b/search_api.views.inc index c242642..757b4d9 100644 --- a/search_api.views.inc +++ b/search_api.views.inc @@ -5,7 +5,12 @@ * Views hook implementations for the Search API module. */ +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\search_api\Entity\Index; +use Drupal\search_api\Item\FieldInterface; +use Drupal\search_api\Property\PropertyInterface; +use Drupal\search_api\SearchApiException; +use Drupal\search_api\Utility; /** * Implements hook_views_data(). @@ -26,19 +31,43 @@ function search_api_views_data() { 'query_id' => 'search_api_query', ); - // @todo Add field, filter, … handlers for all fields. + /** @var \Drupal\search_api\Item\FieldInterface $field */ + foreach ($index->getFields() as $field_id => $field) { + $field_alias = _search_api_views_find_field_alias($field_id, $table); + $field_definition = _search_api_views_get_handlers($field); + if ($field_definition) { + $field_definition += array( + 'title' => $field->getLabel(), + 'help' => $field->getDescription(), + ); + if ($datasource = $field->getDatasource()) { + $field_definition['group'] = $datasource->label(); + } + if ($definition = $field->getDataDefinition()) { + $field_definition['title short'] = $definition->getLabel(); + } + if ($field_id != $field_alias) { + $field_definition['real field'] = $field_id; + } + $table[$field_alias] = $field_definition; + } + } + + if (isset($table['search_api_language']['filter']['id'])) { + $table['search_api_language']['filter']['id'] = 'search_api_language'; + } // Add handlers for special fields. $table['search_api_id']['title'] = t('Entity ID'); $table['search_api_id']['help'] = t("The entity's ID"); $table['search_api_id']['field']['id'] = 'numeric'; - $table['search_api_id']['sort']['id'] = 'search_api_sort'; + $table['search_api_id']['sort']['id'] = 'search_api'; $table['search_api_datasource']['title'] = t('Datasource'); $table['search_api_datasource']['help'] = t("The data source ID"); $table['search_api_datasource']['field']['id'] = 'standard'; - // @todo Enable filtering on datasource. - $table['search_api_datasource']['sort']['id'] = 'search_api_sort'; + $table['search_api_datasource']['filter']['id'] = 'search_api_datasource'; + $table['search_api_datasource']['sort']['id'] = 'search_api'; $table['search_api_relevance']['group'] = t('Search'); $table['search_api_relevance']['title'] = t('Relevance'); @@ -46,7 +75,7 @@ function search_api_views_data() { $table['search_api_relevance']['field']['type'] = 'decimal'; $table['search_api_relevance']['field']['id'] = 'numeric'; $table['search_api_relevance']['field']['click sortable'] = TRUE; - $table['search_api_relevance']['sort']['id'] = 'search_api_sort'; + $table['search_api_relevance']['sort']['id'] = 'search_api'; $table['search_api_excerpt']['group'] = t('Search'); $table['search_api_excerpt']['title'] = t('Excerpt'); @@ -96,3 +125,194 @@ function search_api_views_plugins_row_alter(array &$plugins) { } $plugins['search_api']['base'] = $bases; } + +/** + * Finds an unused field alias for a field in a Views table definition. + * + * @param string $field_id + * The original ID of the Search API field. + * @param array $table + * The Views table definition. + * + * @return string + * The field alias to use. + */ +function _search_api_views_find_field_alias($field_id, array &$table) { + $base = $field_alias = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field_id); + $i = 0; + while (isset($table[$field_alias])) { + $field_alias = $base . '_' . ++$i; + } + return $field_alias; +} + +/** + * Returns the Views handlers to use for a given field. + * + * @param \Drupal\search_api\Item\FieldInterface $field + * The field to add to the definition. + * + * @return array + * The Views definition to add for the given field. + */ +function _search_api_views_get_handlers(FieldInterface $field) { + $mapping = _search_api_views_handler_mapping(); + + try { + $types = array(); + $definition = $field->getDataDefinition(); + + while ($definition instanceof PropertyInterface && $definition != $definition->getWrappedProperty()) { + $definition = $definition->getWrappedProperty(); + } + + if ($definition instanceof FieldDefinitionInterface) { + if ($definition->getType() == 'entity_reference') { + $types[] = 'entity:' . $definition->getSetting('target_type'); + $types[] = 'entity'; + } + } + // @todo Detect fields with fixed lists of options, add type "options". + $types[] = $field->getType(); + /** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */ + $data_type = Utility::getDataTypePluginManager() + ->createInstance($field->getType()); + if (!$data_type->isDefault()) { + $types[] = $data_type->getFallbackType(); + } + + foreach ($types as $type) { + if (isset($mapping[$type])) { + _search_api_views_handler_adjustments($type, $field, $mapping); + return $mapping[$type]; + } + } + } + catch (SearchApiException $e) { + $vars['%index'] = $field->getIndex()->label(); + $vars['%field'] = $field->getPrefixedLabel(); + watchdog_exception('search_api', $e, '%type while Views handlers for field %field on index %index: @message in %function (line %line of %file).', $vars); + } + + return array(); +} + +/** + * Makes necessary, field-specific adjustments to Views handler definitions. + * + * @param string $type + * The type of field, as defined in _search_api_views_handler_mapping(). + * @param \Drupal\search_api\Item\FieldInterface $field + * The field whose handler definitions are being created. + * @param array $mapping + * The handler definitions for the field, as a reference. + */ +function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$mapping) { + $definition = $field->getDataDefinition(); + if ($type == 'entity:taxonomy_term') { + if (isset($definition->getSettings()['handler_settings']['target_bundles'])) { + $target_bundles = $definition->getSettings()['handler_settings']['target_bundles']; + if (count($target_bundles) == 1) { + $mapping[$type]['vocabulary'] = reset($target_bundles); + } + } + } +} + +/** + * Determines the mapping of Search API data types to their Views handlers. + * + * @return array + * An associative array with data types as the keys and Views field data + * definitions as the values. In addition to all normally defined data types, + * keys can also be "options" for any field with an options list, "entity" for + * general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE" + * being the machine name of an entity type) for entities of that type. + * + * @see search_api_views_handler_mapping_alter() + */ +function _search_api_views_handler_mapping() { + $mapping = &drupal_static(__FUNCTION__); + + if (!isset($mapping)) { + $mapping = array( + 'boolean' => array( + 'filter' => array( + 'id' => 'search_api_boolean', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'date' => array( + 'filter' => array( + 'id' => 'search_api_date', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'decimal' => array( + 'filter' => array( + 'id' => 'search_api_numeric', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'integer' => array( + 'filter' => array( + 'id' => 'search_api_numeric', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'string' => array( + 'filter' => array( + 'id' => 'search_api_string', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'text' => array( + 'filter' => array( + 'id' => 'search_api_text', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'options' => array( + 'filter' => array( + 'id' => 'search_api_options', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'entity:taxonomy_term' => array( + 'filter' => array( + 'id' => 'search_api_term', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + 'entity:user' => array( + 'filter' => array( + 'id' => 'search_api_user', + ), + 'sort' => array( + 'id' => 'search_api', + ), + ), + ); + + $alter_id = 'search_api_views_handler_mapping'; + \Drupal::moduleHandler()->alter($alter_id, $mapping); + } + + return $mapping; +} diff --git a/search_api_db/src/Plugin/search_api/backend/Database.php b/search_api_db/src/Plugin/search_api/backend/Database.php index ad7b0b9..64d2c5c 100644 --- a/search_api_db/src/Plugin/search_api/backend/Database.php +++ b/search_api_db/src/Plugin/search_api/backend/Database.php @@ -206,7 +206,7 @@ class Database extends BackendPluginBase { * The data type plugin manager. */ public function getDataTypePluginManager() { - return $this->dataTypePluginManager ?: \Drupal::service('plugin.manager.search_api.data_type'); + return $this->dataTypePluginManager ?: Utility::getDataTypePluginManager(); } /** diff --git a/src/DataType/DataTypePluginBase.php b/src/DataType/DataTypePluginBase.php index 353ffbb..64e4c37 100644 --- a/src/DataType/DataTypePluginBase.php +++ b/src/DataType/DataTypePluginBase.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\DataType; use Drupal\search_api\Plugin\ConfigurablePluginBase; use Drupal\search_api\Backend\BackendPluginManager; +use Drupal\search_api\Utility; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -84,7 +85,7 @@ abstract class DataTypePluginBase extends ConfigurablePluginBase implements Data * The backend plugin manager. */ public function getBackendManager() { - return $this->backendManager ?: \Drupal::service('plugin.manager.search_api.backend'); + return $this->backendManager ?: Utility::getBackendPluginManager(); } /** diff --git a/src/Entity/Index.php b/src/Entity/Index.php index b6907c6..51a94b9 100644 --- a/src/Entity/Index.php +++ b/src/Entity/Index.php @@ -368,8 +368,7 @@ class Index extends ConfigEntityBase implements IndexInterface { public function getDatasources($only_enabled = TRUE) { if (!isset($this->datasourcePlugins)) { $this->datasourcePlugins = array(); - /** @var $datasource_plugin_manager \Drupal\search_api\Datasource\DatasourcePluginManager */ - $datasource_plugin_manager = \Drupal::service('plugin.manager.search_api.datasource'); + $datasource_plugin_manager = Utility::getDatasourcePluginManager(); foreach ($datasource_plugin_manager->getDefinitions() as $name => $datasource_definition) { if (class_exists($datasource_definition['class']) && empty($this->datasourcePlugins[$name])) { @@ -398,7 +397,7 @@ class Index extends ConfigEntityBase implements IndexInterface { * {@inheritdoc} */ public function hasValidTracker() { - return (bool) \Drupal::service('plugin.manager.search_api.tracker')->getDefinition($this->getTrackerId(), FALSE); + return (bool) Utility::getTrackerPluginManager()->getDefinition($this->getTrackerId(), FALSE); } /** @@ -414,7 +413,7 @@ class Index extends ConfigEntityBase implements IndexInterface { public function getTracker() { if (!$this->trackerPlugin) { $tracker_plugin_configuration = array('index' => $this) + $this->tracker_config; - if (!($this->trackerPlugin = \Drupal::service('plugin.manager.search_api.tracker')->createInstance($this->getTrackerId(), $tracker_plugin_configuration))) { + if (!($this->trackerPlugin = Utility::getTrackerPluginManager()->createInstance($this->getTrackerId(), $tracker_plugin_configuration))) { $args['@tracker'] = $this->tracker; $args['%index'] = $this->label(); throw new SearchApiException(new FormattableMarkup('The tracker with ID "@tracker" could not be retrieved for index %index.', $args)); @@ -524,8 +523,7 @@ class Index extends ConfigEntityBase implements IndexInterface { */ protected function loadProcessors() { if (!isset($this->processors)) { - /** @var $processor_plugin_manager \Drupal\search_api\Processor\ProcessorPluginManager */ - $processor_plugin_manager = \Drupal::service('plugin.manager.search_api.processor'); + $processor_plugin_manager = Utility::getProcessorPluginManager(); $processor_settings = $this->getOption('processors', array()); foreach ($processor_plugin_manager->getDefinitions() as $name => $processor_definition) { diff --git a/src/Entity/Server.php b/src/Entity/Server.php index fac5f8f..ae7c973 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -112,7 +112,7 @@ class Server extends ConfigEntityBase implements ServerInterface { * {@inheritdoc} */ public function hasValidBackend() { - $backend_plugin_definition = \Drupal::service('plugin.manager.search_api.backend')->getDefinition($this->getBackendId(), FALSE); + $backend_plugin_definition = Utility::getBackendPluginManager()->getDefinition($this->getBackendId(), FALSE); return !empty($backend_plugin_definition); } @@ -128,7 +128,7 @@ class Server extends ConfigEntityBase implements ServerInterface { */ public function getBackend() { if (!$this->backendPlugin) { - $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend'); + $backend_plugin_manager = Utility::getBackendPluginManager(); $config = $this->backend_config; $config['server'] = $this; if (!($this->backendPlugin = $backend_plugin_manager->createInstance($this->getBackendId(), $config))) { diff --git a/src/Form/IndexForm.php b/src/Form/IndexForm.php index 447f0df..0448881 100644 --- a/src/Form/IndexForm.php +++ b/src/Form/IndexForm.php @@ -98,7 +98,7 @@ class IndexForm extends EntityForm { * The datasource plugin manager. */ protected function getDatasourcePluginManager() { - return $this->datasourcePluginManager ?: \Drupal::service('plugin.manager.search_api.datasource'); + return $this->datasourcePluginManager ?: Utility::getDatasourcePluginManager(); } /** @@ -108,7 +108,7 @@ class IndexForm extends EntityForm { * The tracker plugin manager. */ protected function getTrackerPluginManager() { - return $this->trackerPluginManager ?: \Drupal::service('plugin.manager.search_api.tracker'); + return $this->trackerPluginManager ?: Utility::getTrackerPluginManager(); } /** diff --git a/src/Form/ServerForm.php b/src/Form/ServerForm.php index 862da9e..5e1dd61 100644 --- a/src/Form/ServerForm.php +++ b/src/Form/ServerForm.php @@ -15,6 +15,7 @@ use Drupal\Core\Url; use Drupal\search_api\Backend\BackendPluginManager; use Drupal\search_api\SearchApiException; use Drupal\search_api\ServerInterface; +use Drupal\search_api\Utility; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -77,7 +78,7 @@ class ServerForm extends EntityForm { * The backend plugin manager. */ protected function getBackendPluginManager() { - return $this->backendPluginManager ?: \Drupal::service('plugin.manager.search_api.backend'); + return $this->backendPluginManager ?: Utility::getBackendPluginManager(); } /** diff --git a/src/Plugin/search_api/datasource/ContentEntity.php b/src/Plugin/search_api/datasource/ContentEntity.php index df5525b..aa3e875 100644 --- a/src/Plugin/search_api/datasource/ContentEntity.php +++ b/src/Plugin/search_api/datasource/ContentEntity.php @@ -17,6 +17,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\TypedDataManager; +use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldConfigInterface; use Drupal\search_api\Datasource\DatasourcePluginBase; use Drupal\search_api\Entity\Index; diff --git a/src/Plugin/views/filter/SearchApiFilter.php b/src/Plugin/views/filter/SearchApiFilter.php deleted file mode 100644 index 9f970b9..0000000 --- a/src/Plugin/views/filter/SearchApiFilter.php +++ /dev/null @@ -1,116 +0,0 @@ - $this->t('Is less than'), - '<=' => $this->t('Is less than or equal to'), - '=' => $this->t('Is equal to'), - '<>' => $this->t('Is not equal to'), - '>=' => $this->t('Is greater than or equal to'), - '>' => $this->t('Is greater than'), - 'empty' => $this->t('Is empty'), - 'not empty' => $this->t('Is not empty'), - ); - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - // @todo Hopefully we can now be more sure of what we get in $this->value. - while (is_array($this->value) && count($this->value) < 2) { - $this->value = $this->value ? reset($this->value) : NULL; - } - $form['value'] = array( - '#type' => 'textfield', - '#title' => !$form_state->get('exposed') ? $this->t('Value') : '', - '#size' => 30, - '#default_value' => isset($this->value) ? $this->value : '', - ); - - // Hide the value box if the operator is 'empty' or 'not empty'. - // Radios share the same selector so we have to add some dummy selector. - if (!$form_state->get('exposed')) { - $form['value']['#states']['visible'] = array( - ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), - ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), - ); - } - elseif (!empty($this->options['expose']['use_operator'])) { - $name = $this->options['expose']['operator_id']; - $form['value']['#states']['visible'] = array( - ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'), - ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'), - ); - } - } - - /** - * {@inheritdoc} - */ - public function adminSummary() { - if (!empty($this->options['exposed'])) { - return $this->t('exposed'); - } - - if ($this->operator === 'empty') { - return $this->t('is empty'); - } - if ($this->operator === 'not empty') { - return $this->t('is not empty'); - } - - return Html::escape((string) $this->operator) . ' ' . Html::escape((string) $this->value); - } - - /** - * {@inheritdoc} - */ - public function query() { - if ($this->operator === 'empty') { - $this->query->condition($this->realField, NULL, '=', $this->options['group']); - } - elseif ($this->operator === 'not empty') { - $this->query->condition($this->realField, NULL, '<>', $this->options['group']); - } - else { - while (is_array($this->value)) { - $this->value = $this->value ? reset($this->value) : NULL; - } - if (strlen($this->value) > 0) { - $this->query->condition($this->realField, $this->value, $this->operator, $this->options['group']); - } - } - } - -} diff --git a/src/Plugin/views/filter/SearchApiFilterBoolean.php b/src/Plugin/views/filter/SearchApiFilterBoolean.php index c18fe16..a0f1c45 100644 --- a/src/Plugin/views/filter/SearchApiFilterBoolean.php +++ b/src/Plugin/views/filter/SearchApiFilterBoolean.php @@ -7,7 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; -use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\views\filter\BooleanOperator; /** * Defines a filter for filtering on boolean values. @@ -16,28 +16,8 @@ use Drupal\Core\Form\FormStateInterface; * * @ViewsFilter("search_api_boolean") */ -class SearchApiFilterBoolean extends SearchApiFilter { +class SearchApiFilterBoolean extends BooleanOperator { - /** - * {@inheritdoc} - */ - public function operatorOptions() { - return array(); - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - while (is_array($this->value)) { - $this->value = $this->value ? array_shift($this->value) : NULL; - } - $form['value'] = array( - '#type' => 'select', - '#title' => !$form_state->get('exposed') ? $this->t('Value') : '', - '#options' => array(1 => $this->t('True'), 0 => $this->t('False')), - '#default_value' => isset($this->value) ? $this->value : '', - ); - } + use SearchApiFilterTrait; } diff --git a/src/Plugin/views/filter/SearchApiFilterDatasource.php b/src/Plugin/views/filter/SearchApiFilterDatasource.php new file mode 100644 index 0000000..80b9229 --- /dev/null +++ b/src/Plugin/views/filter/SearchApiFilterDatasource.php @@ -0,0 +1,35 @@ +valueOptions)) { + return $this->valueOptions; + } + + $this->valueOptions = array(); + + $index = $this->getIndex(); + if ($index) { + foreach ($index->getDatasources() as $datasource_id => $datasource) { + $this->valueOptions[$datasource_id] = $datasource->label(); + } + } + + return $this->valueOptions; + } + +} diff --git a/src/Plugin/views/filter/SearchApiFilterDate.php b/src/Plugin/views/filter/SearchApiFilterDate.php index 1566d81..d684c77 100644 --- a/src/Plugin/views/filter/SearchApiFilterDate.php +++ b/src/Plugin/views/filter/SearchApiFilterDate.php @@ -7,7 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; -use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\views\filter\Date; /** * Defines a filter for filtering on dates. @@ -16,87 +16,19 @@ use Drupal\Core\Form\FormStateInterface; * * @ViewsFilter("search_api_date") */ -class SearchApiFilterDate extends SearchApiFilter { +class SearchApiFilterDate extends Date { - /** - * {@inheritdoc} - */ - public function defineOptions() { - return parent::defineOptions() + array( - 'widget_type' => array('default' => 'default'), - ); - } - - /** - * {@inheritdoc} - */ - public function hasExtraOptions() { - if (\Drupal::moduleHandler()->moduleExists('date_popup')) { - return TRUE; - } - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) { - parent::buildExtraOptionsForm($form, $form_state); - if (\Drupal::moduleHandler()->moduleExists('date_popup')) { - $widget_options = array( - 'default' => $this->t('Default'), - 'date_popup' => $this->t('Date popup'), - ); - $form['widget_type'] = array( - '#type' => 'radios', - '#title' => $this->t('Date selection form element'), - '#default_value' => $this->options['widget_type'], - '#options' => $widget_options, - ); - } - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - parent::valueForm($form, $form_state); - - // If we are using the date popup widget, overwrite the settings of the form - // according to what date_popup expects. - if ($this->options['widget_type'] == 'date_popup' && \Drupal::moduleHandler()->moduleExists('date_popup')) { - $form['value']['#type'] = 'date_popup'; - $form['value']['#date_format'] = 'm/d/Y'; - unset($form['value']['#description']); - } - elseif (!$form_state->get('exposed')) { - $form['value']['#description'] = $this->t('A date in any format understood by PHP. For example, "@date1" or "@date2".', array( - '@doc-link' => 'http://php.net/manual/en/function.strtotime.php', - '@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'), - '@date2' => 'now + 1 day', - )); - } - } + use SearchApiFilterTrait; /** * {@inheritdoc} */ - public function query() { - if ($this->operator === 'empty') { - $this->query->condition($this->realField, NULL, '=', $this->options['group']); - } - elseif ($this->operator === 'not empty') { - $this->query->condition($this->realField, NULL, '<>', $this->options['group']); - } - else { - while (is_array($this->value)) { - $this->value = $this->value ? reset($this->value) : NULL; - } - $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME); - if ($v !== FALSE) { - $this->query->condition($this->realField, $v, $this->operator, $this->options['group']); - } - } + public function operators() { + $operators = parent::operators(); + // @todo Enable "(not) between" again once that operator is available in + // the Search API. + unset($operators['between'], $operators['not between'], $operators['regular_expression']); + return $operators; } } diff --git a/src/Plugin/views/filter/SearchApiFilterEntityBase.php b/src/Plugin/views/filter/SearchApiFilterEntityBase.php deleted file mode 100644 index f05e486..0000000 --- a/src/Plugin/views/filter/SearchApiFilterEntityBase.php +++ /dev/null @@ -1,219 +0,0 @@ - $this->isMultiValued() ? $this->t('Is one of') : $this->t('Is'), - 'all of' => $this->t('Is all of'), - '<>' => $this->isMultiValued() ? $this->t('Is not one of') : $this->t('Is not'), - 'empty' => $this->t('Is empty'), - 'not empty' => $this->t('Is not empty'), - ); - if (!$this->isMultiValued()) { - unset($operators['all of']); - } - return $operators; - } - - /** - * {@inheritdoc} - */ - public function defineOptions() { - $options = parent::defineOptions(); - - $options['expose']['multiple']['default'] = TRUE; - - return $options; - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - parent::valueForm($form, $form_state); - - if (!is_array($this->value)) { - $this->value = $this->value ? array($this->value) : array(); - } - - // Set the correct default value in case the admin-set value is used (and a - // value is present). The value is used if the form is either not exposed, - // or the exposed form wasn't submitted yet. (There doesn't seem to be an - // easier way to check for that.) - if ($this->value && (!$form_state->getUserInput() || !empty($form_state->getUserInput()['live_preview']))) { - $form['value']['#default_value'] = $this->idsToString($this->value); - } - } - - /** - * {@inheritdoc} - */ - public function valueValidate($form, FormStateInterface $form_state) { - if (!empty($form['value'])) { - $form_values = $form_state->getValues(); - $value = $form_values['options']['value']; - $values = $this->isMultiValued($form_values['options']) ? Tags::explode($value) : array($value); - $ids = $this->validateEntityStrings($form['value'], $values, $form_state); - - if ($ids) { - $value = $ids; - $form_state->setValue('value', $value); - } - } - } - - /** - * {@inheritdoc} - */ - public function acceptExposedInput($input) { - $rc = parent::acceptExposedInput($input); - - if ($rc) { - // If we have previously validated input, override. - if ($this->validatedExposedInput) { - $this->value = $this->validatedExposedInput; - } - } - - return $rc; - } - - /** - * {@inheritdoc} - */ - public function validateExposed(&$form, FormStateInterface $form_state) { - if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { - return; - } - - $identifier = $this->options['expose']['identifier']; - $input = $form_state->getValues()[$identifier]; - - if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { - $this->operator = $this->options['group_info']['group_items'][$input]['operator']; - $input = $this->options['group_info']['group_items'][$input]['value']; - } - - $values = $this->isMultiValued() ? Tags::explode($input) : array($input); - - if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) { - $this->validatedExposedInput = $this->validateEntityStrings($form[$identifier], $values, $form_state); - } - else { - $this->validatedExposedInput = FALSE; - } - } - - /** - * Determines whether multiple values can be entered into this filter. - * - * This is either the case if the form isn't exposed, or if the " Allow - * multiple selections" option is enabled. - * - * @param array $options - * (optional) The options array to use. If not supplied, the options set on - * this filter will be used. - * - * @return bool - * TRUE if multiple values can be entered for this filter, FALSE otherwise. - */ - protected function isMultiValued(array $options = array()) { - $options = $options ?: $this->options; - return empty($options['exposed']) || !empty($options['expose']['multiple']); - } - - /** - * {@inheritdoc} - */ - public function adminSummary() { - if (!is_array($this->value)) { - $this->value = $this->value ? array($this->value) : array(); - } - $value = $this->value; - $this->value = empty($value) ? '' : $this->idsToString($value); - $ret = parent::adminSummary(); - $this->value = $value; - return $ret; - } - - /** - * {@inheritdoc} - */ - public function query() { - if ($this->operator === 'empty') { - $this->query->condition($this->realField, NULL, '=', $this->options['group']); - } - elseif ($this->operator === 'not empty') { - $this->query->condition($this->realField, NULL, '<>', $this->options['group']); - } - elseif (is_array($this->value)) { - $all_of = $this->operator === 'all of'; - $operator = $all_of ? '=' : $this->operator; - if (count($this->value) == 1) { - $this->query->condition($this->realField, reset($this->value), $operator, $this->options['group']); - } - else { - $filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR'); - foreach ($this->value as $value) { - $filter->condition($this->realField, $value, $operator); - } - $this->query->filter($filter, $this->options['group']); - } - } - } - -} diff --git a/src/Plugin/views/filter/SearchApiFilterNumeric.php b/src/Plugin/views/filter/SearchApiFilterNumeric.php new file mode 100644 index 0000000..9f67b30 --- /dev/null +++ b/src/Plugin/views/filter/SearchApiFilterNumeric.php @@ -0,0 +1,41 @@ +query->condition($this->realField, NULL, $this->operator == 'empty' ? '=' : '<>', $this->options['group']); + } + +} diff --git a/src/Plugin/views/filter/SearchApiFilterOptions.php b/src/Plugin/views/filter/SearchApiFilterOptions.php index b42b173..6235505 100644 --- a/src/Plugin/views/filter/SearchApiFilterOptions.php +++ b/src/Plugin/views/filter/SearchApiFilterOptions.php @@ -7,9 +7,8 @@ namespace Drupal\search_api\Plugin\views\filter; -use Drupal\Component\Utility\Html; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Form\FormStateInterface; +use Drupal\views\Plugin\views\filter\ManyToOne; /** * Defines a filter for filtering on fields with a fixed set of possible values. @@ -18,279 +17,17 @@ use Drupal\Core\Form\FormStateInterface; * * @ViewsFilter("search_api_options") */ -class SearchApiFilterOptions extends SearchApiFilter { +class SearchApiFilterOptions extends ManyToOne { - /** - * Stores the values which are available on the form. - * - * @var array|null - */ - protected $valueOptions; - - /** - * The type of form element used to display the options. - * - * @var string - */ - protected $valueFormType = 'checkboxes'; - - /** - * Fills SearchApiFilterOptions::$valueOptions with all possible options. - */ - protected function getValueOptions() { - if (isset($this->valueOptions)) { - return; - } - - // @todo This obviously needs a different solution. - $wrapper = $this->get_wrapper(); - if ($wrapper) { - $this->valueOptions = $wrapper->optionsList('view'); - } - else { - $this->valueOptions = array(); - } - } - - /** - * {@inheritdoc} - */ - public function operatorOptions() { - $options = array( - '=' => $this->t('Is one of'), - 'all of' => $this->t('Is all of'), - '<>' => $this->t('Is none of'), - 'empty' => $this->t('Is empty'), - 'not empty' => $this->t('Is not empty'), - ); - // "Is all of" doesn't make sense for single-valued fields. - if (empty($this->definition['multi-valued'])) { - unset($options['all of']); - } - return $options; - } - - /** - * {@inheritdoc} - */ - public function defineOptions() { - $options = parent::defineOptions(); - $options['expose']['contains']['reduce'] = array('default' => FALSE); - return $options; - } - - /** - * {@inheritdoc} - */ - public function defaultExposeOptions() { - parent::defaultExposeOptions(); - $this->options['expose']['reduce'] = FALSE; - } - - /** - * {@inheritdoc} - */ - public function buildExposedForm(&$form, FormStateInterface $form_state) { - parent::buildExposedForm($form, $form_state); - $form['expose']['reduce'] = array( - '#type' => 'checkbox', - '#title' => $this->t('Limit list to selected items'), - '#description' => $this->t('If checked, the only items presented to the user will be the ones selected here.'), - '#default_value' => !empty($this->options['expose']['reduce']), - ); - } + use SearchApiFilterTrait; /** * {@inheritdoc} */ - protected function valueForm(&$form, FormStateInterface $form_state) { - $this->getValueOptions(); - if (!empty($this->options['expose']['reduce']) && $form_state->get('exposed')) { - $options = $this->reduceValueOptions(); - } - else { - $options = $this->valueOptions; - } - - $form['value'] = array( - '#type' => $this->valueFormType, - '#title' => !$form_state->get('exposed') ? $this->t('Value') : '', - '#options' => $options, - '#multiple' => TRUE, - '#size' => min(4, count($options)), - '#default_value' => is_array($this->value) ? $this->value : array(), - ); - - // Hide the value box if the operator is 'empty' or 'not empty'. - // Radios share the same selector so we have to add some dummy selector. - if (!$form_state->get('exposed')) { - $form['value']['#states']['visible'] = array( - ':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'), - ':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'), - ); - } - elseif (!empty($this->options['expose']['use_operator'])) { - $name = $this->options['expose']['operator_id']; - $form['value']['#states']['visible'] = array( - ':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'), - ':input[name="' . $name . '"],dummy-not-empty' => array('!value' => 'not empty'), - ); - } - } - - /** - * Retrieves the reduced options list to use for the exposed filter. - * - * @return string[] - * An options list for the values list, with only the ones selected in the - * admin UI included. - */ - protected function reduceValueOptions() { - foreach ($this->valueOptions as $id => $option) { - if (!isset($this->options['value'][$id])) { - unset($this->valueOptions[$id]); - } - } - return $this->valueOptions; - } - - /** - * {@inheritdoc} - */ - // @todo Is this still needed in D8? - public function valueSubmit($form, FormStateInterface $form_state) { - // Drupal's FAPI system automatically puts '0' in for any checkbox that - // was not set, and the key to the checkbox if it is set. - // Unfortunately, this means that if the key to that checkbox is 0, - // we are unable to tell if that checkbox was set or not. - - // Luckily, the '#value' on the checkboxes form actually contains - // *only* a list of checkboxes that were set, and we can use that - // instead. - - $form_state->setValueForElement($form['value'], $form['value']['#value']); - } - - /** - * {@inheritdoc} - */ - public function adminSummary() { - if (!empty($this->options['exposed'])) { - return $this->t('exposed'); - } - - if ($this->operator === 'empty') { - return $this->t('is empty'); - } - if ($this->operator === 'not empty') { - return $this->t('is not empty'); - } - - if (!is_array($this->value)) { - return ''; - } - - $operator_options = $this->operatorOptions(); - $operator = $operator_options[$this->operator]; - $values = ''; - - // Remove every element which is not known. - // @todo Why? Doesn't FAPI already prevent this? - $this->getValueOptions(); - foreach ($this->value as $i => $value) { - if (!isset($this->valueOptions[$value])) { - unset($this->value[$i]); - } - } - // Choose different kind of ouput for 0, a single and multiple values. - if (count($this->value) == 0) { - return $this->operator != '<>' ? $this->t('none') : $this->t('any'); - } - elseif (count($this->value) == 1) { - switch ($this->operator) { - case '=': - case 'all of': - $operator = '='; - break; - - case '<>': - $operator = '<>'; - break; - } - // If there is only a single value, use just the plain operator, = or <>. - $operator = Html::escape($operator); - $values = Html::escape($this->valueOptions[reset($this->value)]); - } - else { - foreach ($this->value as $value) { - if ($values !== '') { - $values .= ', '; - } - if (Unicode::strlen($values) > 20) { - $values .= '…'; - break; - } - $values .= Html::escape($this->valueOptions[$value]); - } - } - - return $operator . (($values !== '') ? ' ' . $values : ''); - } - - /** - * {@inheritdoc} - */ - public function query() { - if ($this->operator === 'empty') { - $this->query->condition($this->realField, NULL, '=', $this->options['group']); - return; - } - if ($this->operator === 'not empty') { - $this->query->condition($this->realField, NULL, '<>', $this->options['group']); - return; - } - - // Extract the value. - while (is_array($this->value) && count($this->value) == 1) { - $this->value = reset($this->value); - } - - // Determine operator and conjunction. The defaults are already right for - // "all of". - $operator = '='; - $conjunction = 'AND'; - switch ($this->operator) { - case '=': - $conjunction = 'OR'; - break; - - case '<>': - $operator = '<>'; - break; - } - - // If the value is an empty array, we either want no filter at all (for - // "is none of"), or want to find only items with no value for the field. - if ($this->value === array()) { - if ($operator != '<>') { - $this->query->condition($this->realField, NULL, '=', $this->options['group']); - } - return; - } + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); - if (is_scalar($this->value) && $this->value !== '') { - $this->query->condition($this->realField, $this->value, $operator, $this->options['group']); - } - elseif ($this->value) { - $filter = $this->query->createFilter($conjunction); - // $filter will be NULL if there were errors in the query. - if ($filter) { - foreach ($this->value as $v) { - $filter->condition($this->realField, $v, $operator); - } - $this->query->filter($filter, $this->options['group']); - } - } + unset($form['reduce_duplicates']); } } diff --git a/src/Plugin/views/filter/SearchApiFilterString.php b/src/Plugin/views/filter/SearchApiFilterString.php new file mode 100644 index 0000000..992db5e --- /dev/null +++ b/src/Plugin/views/filter/SearchApiFilterString.php @@ -0,0 +1,24 @@ + $this->t('contains'), - '<>' => $this->t("doesn't contain"), - ); + public function operators() { + $operators = parent::operators(); + + $operators['=']['title'] = $this->t('contains'); + $operators['!=']['title'] = $this->t("doesn't contain"); + + $operators = array_intersect_key($operators, array('=' => 1, '!=' => 1)); + + return $operators; } } diff --git a/src/Plugin/views/filter/SearchApiFilterTrait.php b/src/Plugin/views/filter/SearchApiFilterTrait.php new file mode 100644 index 0000000..6cc782d --- /dev/null +++ b/src/Plugin/views/filter/SearchApiFilterTrait.php @@ -0,0 +1,99 @@ +definition['allow empty'])) { + $this->definition['allow empty'] = TRUE; + } + } + + /** + * Overrides the Views handlers' ensureMyTable() method. + * + * This is done since this is not necessary for Search API queries. + */ + public function ensureMyTable() { + // Do nothing. + } + + /** + * Adds a form for entering the value or values for the filter. + * + * Overridden to remove fields that won't be used (but aren't hidden either + * because of a small bug/glitch in the form code. + * + * @see \Drupal\views\Plugin\views\filter\FilterPluginBase::valueForm() + */ + protected function valueForm(&$form, FormStateInterface $form_state) { + parent::valueForm($form, $form_state); + + if (isset($form['value']['min'])) { + if (!$this->operatorValues(2)) { + unset($form['value']['min'], $form['value']['max']); + } + } + } + + /** + * Returns the active search index. + * + * @return \Drupal\search_api\IndexInterface|null + * The search index to use with this filter, or NULL if none could be + * loaded. + */ + protected function getIndex() { + if ($this->query) { + return $this->query->getIndex(); + } + $base_table = $this->view->storage->get('base_table'); + return SearchApiQuery::getIndexFromTable($base_table); + } + + /** + * Adds a filter to the search query. + * + * Overridden to avoid errors because of SQL-specific functionality being used + * when "Many To One" is used as a base class. + * + * @see \Drupal\views\Plugin\views\filter\ManyToOne::opHelper() + */ + protected function opHelper() { + if (empty($this->value)) { + return; + } + // @todo Use "IN"/"NOT IN" instead, once available. + $conjunction = $this->operator == 'or' ? 'OR' : 'AND'; + $operator = $this->operator == 'not' ? '<>' : '='; + $filter = $this->query->createFilter($conjunction); + foreach ($this->value as $value) { + $filter->condition($this->realField, $value, $operator); + } + $this->query->filter($filter, $this->options['group']); + } + +} diff --git a/src/Plugin/views/filter/SearchApiFulltext.php b/src/Plugin/views/filter/SearchApiFulltext.php index c00edb2..8747a6b 100644 --- a/src/Plugin/views/filter/SearchApiFulltext.php +++ b/src/Plugin/views/filter/SearchApiFulltext.php @@ -32,11 +32,26 @@ class SearchApiFulltext extends SearchApiFilterText { /** * {@inheritdoc} */ - public function operatorOptions() { + public function operators() { return array( - 'AND' => $this->t('Contains all of these words'), - 'OR' => $this->t('Contains any of these words'), - 'NOT' => $this->t('Contains none of these words'), + 'and' => array( + 'title' => $this->t('Contains all of these words'), + 'method' => 'opFulltextSearch', + 'short' => $this->t('and'), + 'values' => 1, + ), + 'or' => array( + 'title' => $this->t('Contains any of these words'), + 'method' => 'opFulltextSearch', + 'short' => $this->t('or'), + 'values' => 1, + ), + 'not' => array( + 'title' => $this->t('Contains none of these words'), + 'method' => 'opFulltextSearch', + 'short' => $this->t('not'), + 'values' => 1, + ), ); } @@ -46,7 +61,7 @@ class SearchApiFulltext extends SearchApiFilterText { public function defineOptions() { $options = parent::defineOptions(); - $options['operator']['default'] = 'AND'; + $options['operator']['default'] = 'and'; $options['mode']['default'] = 'keys'; $options['min_length']['default'] = ''; @@ -146,7 +161,7 @@ class SearchApiFulltext extends SearchApiFilterText { /** * {@inheritdoc} */ - public function query() { + public function opFulltextSearch() { while (is_array($this->value)) { $this->value = $this->value ? reset($this->value) : ''; } @@ -169,7 +184,7 @@ class SearchApiFulltext extends SearchApiFilterText { if ($filter) { $filter = $this->query->createFilter('OR'); - $op = $this->operator === 'NOT' ? '<>' : '='; + $op = $this->operator === 'not' ? '<>' : '='; foreach ($fields as $field) { $filter->condition($field, $this->value, $op); } @@ -179,7 +194,7 @@ class SearchApiFulltext extends SearchApiFilterText { // If the operator was set to OR or NOT, set OR as the conjunction. (It is // also set for NOT since otherwise it would be "not all of these words".) - if ($this->operator != 'AND') { + if ($this->operator != 'and') { $this->query->setOption('conjunction', 'OR'); } @@ -187,7 +202,7 @@ class SearchApiFulltext extends SearchApiFilterText { $old = $this->query->getKeys(); $old_original = $this->query->getOriginalKeys(); $this->query->keys($this->value); - if ($this->operator == 'NOT') { + if ($this->operator == 'not') { $keys = &$this->query->getKeys(); if (is_array($keys)) { $keys['#negation'] = TRUE; diff --git a/src/Plugin/views/filter/SearchApiLanguage.php b/src/Plugin/views/filter/SearchApiLanguage.php index 31eb689..b97031c 100644 --- a/src/Plugin/views/filter/SearchApiLanguage.php +++ b/src/Plugin/views/filter/SearchApiLanguage.php @@ -8,6 +8,8 @@ namespace Drupal\search_api\Plugin\views\filter; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a filter for filtering on the language of items. @@ -19,35 +21,86 @@ use Drupal\Core\Language\LanguageInterface; class SearchApiLanguage extends SearchApiFilterOptions { /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface|null + */ + protected $languageManager; + + /** * {@inheritdoc} */ - protected function getValueOptions() { - parent::getValueOptions(); + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /** @var static $plugin */ + $plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition); + + /** @var $language_manager \Drupal\Core\Language\LanguageManagerInterface */ + $language_manager = $container->get('language_manager'); + $plugin->setLanguageManager($language_manager); + + return $plugin; + } + + /** + * Retrieves the language manager. + * + * @return \Drupal\Core\Language\LanguageManagerInterface + * The language manager. + */ + public function getLanguageManager() { + return $this->languageManager ?: \Drupal::languageManager(); + } + + /** + * Sets the language manager. + * + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The new language manager. + * + * @return $this + */ + public function setLanguageManager(LanguageManagerInterface $language_manager) { + $this->languageManager = $language_manager; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getValueOptions() { + if (isset($this->valueOptions)) { + return $this->valueOptions; + } + $this->valueOptions = array( 'content' => $this->t('Current content language'), 'interface' => $this->t('Current interface language'), 'default' => $this->t('Default site language'), - ) + $this->valueOptions; + ); + + foreach ($this->getLanguageManager()->getLanguages(LanguageInterface::STATE_ALL) as $langcode => $language) { + $this->valueOptions[$langcode] = $language->getName(); + } + + return $this->valueOptions; } /** * {@inheritdoc} */ public function query() { - if (!is_array($this->value)) { - $this->value = $this->value ? array($this->value) : array(); - } - foreach ($this->value as $i => $v) { - if ($v == 'content') { - $this->value[$i] = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + foreach ($this->value as $i => $value) { + if ($value == 'content') { + $this->value[$i] = $this->getLanguageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); } - elseif ($v == 'interface') { - $this->value[$i] = \Drupal::languageManager()->getCurrentLanguage()->getId(); + elseif ($value == 'interface') { + $this->value[$i] = $this->getLanguageManager()->getCurrentLanguage()->getId(); } - elseif ($v == 'default') { - $this->value[$i] = \Drupal::languageManager()->getDefaultLanguage()->getId(); + elseif ($value == 'default') { + $this->value[$i] = $this->getLanguageManager()->getDefaultLanguage()->getId(); } } + parent::query(); } diff --git a/src/Plugin/views/filter/SearchApiTerm.php b/src/Plugin/views/filter/SearchApiTerm.php index 9841587..c5aafee 100644 --- a/src/Plugin/views/filter/SearchApiTerm.php +++ b/src/Plugin/views/filter/SearchApiTerm.php @@ -7,8 +7,9 @@ namespace Drupal\search_api\Plugin\views\filter; -use Drupal\Core\Form\FormStateInterface; -use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\taxonomy\Plugin\views\filter\TaxonomyIndexTid; +use Drupal\taxonomy\TermStorageInterface; +use Drupal\taxonomy\VocabularyStorageInterface; /** * Defines a filter for filtering on taxonomy term references. @@ -19,295 +20,19 @@ use Drupal\taxonomy\Entity\Vocabulary; * * @ViewsFilter("search_api_term") */ -// @todo Needs updating, especially the DB queries that merge on vocabulary. -class SearchApiTerm extends SearchApiFilterEntityBase { - - /** - * {@inheritdoc} - */ - public function hasExtraOptions() { - return !empty($this->definition['vocabulary']); - } - - /** - * {@inheritdoc} - */ - public function defineOptions() { - $options = parent::defineOptions(); - - $options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select'); - $options['hierarchy'] = array('default' => 0); - $options['error_message'] = array('default' => TRUE, 'bool' => TRUE); - - return $options; - } - - /** - * {@inheritdoc} - */ - public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) { - $form['type'] = array( - '#type' => 'radios', - '#title' => $this->t('Selection type'), - '#options' => array('select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')), - '#default_value' => $this->options['type'], - ); - - $form['hierarchy'] = array( - '#type' => 'checkbox', - '#title' => $this->t('Show hierarchy in dropdown'), - '#default_value' => !empty($this->options['hierarchy']), - ); - $form['hierarchy']['#states']['visible'][':input[name="options[type]"]']['value'] = 'select'; - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - parent::valueForm($form, $form_state); - - if (!empty($this->definition['vocabulary'])) { - $vocabulary = Vocabulary::load($this->definition['vocabulary']); - $title = $this->t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->label())); - } - else { - $vocabulary = FALSE; - $title = $this->t('Select terms'); - } - $form['value']['#title'] = $title; - - if ($vocabulary && $this->options['type'] == 'textfield') { - $form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->id(); - } - else { - if ($vocabulary && !empty($this->options['hierarchy'])) { - /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */ - $term_storage = \Drupal::entityManager()->getStorage('taxonomy_term'); - $tree = $term_storage->loadTree($vocabulary->id()); - $options = array(); - - if ($tree) { - foreach ($tree as $term) { - $choice = new \stdClass(); - $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name); - $options[] = $choice; - } - } - } - else { - $options = array(); - $query = Database::getConnection()->select('taxonomy_term_data', 'td'); - $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); - $query->fields('td'); - $query->orderby('tv.weight'); - $query->orderby('tv.name'); - $query->orderby('td.weight'); - $query->orderby('td.name'); - $query->addTag('term_access'); - if ($vocabulary) { - $query->condition('tv.vid', $vocabulary->id()); - } - $result = $query->execute(); - foreach ($result as $term) { - $options[$term->tid] = $term->name; - } - } - - $default_value = (array) $this->value; - - if ($form_state->get('exposed')) { - $identifier = $this->options['expose']['identifier']; - - if (!empty($this->options['expose']['reduce'])) { - $options = $this->reduceValueOptions($options); - - if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) { - $default_value = array(); - } - } - - if (empty($this->options['expose']['multiple'])) { - if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) { - $default_value = 'All'; - } - elseif (empty($default_value)) { - $keys = array_keys($options); - $default_value = array_shift($keys); - } - // Due to #1464174 there is a chance that array('') was saved in the - // admin ui. Let's choose a safe default value. - elseif ($default_value == array('')) { - $default_value = 'All'; - } - else { - $copy = $default_value; - $default_value = array_shift($copy); - } - } - } - $form['value']['#type'] = 'select'; - $form['value']['#multiple'] = TRUE; - $form['value']['#options'] = $options; - $form['value']['#size'] = min(9, count($options)); - $form['value']['#default_value'] = $default_value; - - $input = &$form_state->getUserInput(); - if ($form_state->get('exposed') && isset($identifier) && !isset($input[$identifier])) { - $input[$identifier] = $default_value; - } - } - } - - /** - * Reduces the available exposed options according to the selection. - * - * @param array $options - * The original options list. - * - * @return array - * A reduced version of the options list. - */ - protected function reduceValueOptions(array $options) { - foreach ($options as $id => $option) { - if (empty($this->options['value'][$id])) { - unset($options[$id]); - } - } - return $options; - } - - /** - * {@inheritdoc} - */ - public function valueValidate($form, FormStateInterface $form_state) { - // We only validate if they've chosen the text field style. - if ($this->options['type'] != 'textfield') { - return; - } - - parent::valueValidate($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function acceptExposedInput($input) { - if (empty($this->options['exposed'])) { - return TRUE; - } - - // If view is an attachment and is inheriting exposed filters, then assume - // exposed input has already been validated. - if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) { - $this->validatedExposedInput = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']]; - } - - // If it's non-required and there's no value don't bother filtering. - if (!$this->options['expose']['required'] && empty($this->validatedExposedInput)) { - return FALSE; - } - - return parent::acceptExposedInput($input); - } - - /** - * {@inheritdoc} - */ - public function validateExposed(&$form, FormStateInterface $form_state) { - if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) { - return; - } - - // We only validate if they've chosen the text field style. - if ($this->options['type'] != 'textfield') { - $input = $form_state->getValues()[$this->options['expose']['identifier']]; - if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { - $input = $this->options['group_info']['group_items'][$input]['value']; - } - - if ($input != 'All') { - $this->validatedExposedInput = (array) $input; - } - return; - } - - parent::validateExposed($form, $form_state); - } - - /** - * {@inheritdoc} - */ - protected function validateEntityStrings(array &$form, array $values, FormStateInterface $form_state) { - if (empty($values)) { - return array(); - } - - $tids = array(); - $names = array(); - $missing = array(); - foreach ($values as $value) { - $missing[strtolower($value)] = TRUE; - $names[] = $value; - } - - if (!$names) { - return array(); - } - - $query = Database::getConnection()->select('taxonomy_term_data', 'td'); - $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid'); - $query->fields('td'); - $query->condition('td.name', $names); - if (!empty($this->definition['vocabulary'])) { - $query->condition('tv.id', $this->definition['vocabulary']); - } - $query->addTag('term_access'); - $result = $query->execute(); - foreach ($result as $term) { - unset($missing[strtolower($term->name)]); - $tids[] = $term->tid; - } - - if ($missing) { - if (!empty($this->options['error_message'])) { - $form_state->setError($form, $this->formatPlural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing))))); - } - else { - // Add a bogus TID which will show an empty result for a positive filter - // and be ignored for an excluding one. - $tids[] = 0; - } - } - - return $tids; - } - - /** - * {@inheritdoc} - */ - public function buildExposeForm(&$form, FormStateInterface $form_state) { - parent::buildExposeForm($form, $form_state); - if ($this->options['type'] != 'select') { - unset($form['expose']['reduce']); - } - $form['error_message'] = array( - '#type' => 'checkbox', - '#title' => $this->t('Display error message'), - '#description' => $this->t('Display an error message if one of the entered terms could not be found.'), - '#default_value' => !empty($this->options['error_message']), - ); - } - - /** - * {@inheritdoc} - */ - protected function idsToString(array $ids) { - return implode(', ', Database::getConnection()->select('taxonomy_term_data', 'td') - ->fields('td', array('name')) - ->condition('td.tid', array_filter($ids)) - ->execute() - ->fetchCol()); - } +class SearchApiTerm extends TaxonomyIndexTid { + + use SearchApiFilterTrait; + +// /** +// * {@inheritdoc} +// */ +// public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage) { +// parent::__construct($configuration, $plugin_id, $plugin_definition, $vocabulary_storage, $term_storage); +// +// if (!isset($this->definition['allow empty'])) { +// $this->definition['allow empty'] = TRUE; +// } +// } } diff --git a/src/Plugin/views/filter/SearchApiUser.php b/src/Plugin/views/filter/SearchApiUser.php index 765c12f..8c77ed0 100644 --- a/src/Plugin/views/filter/SearchApiUser.php +++ b/src/Plugin/views/filter/SearchApiUser.php @@ -7,9 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Database\Database; -use Drupal\Core\Form\FormStateInterface; +use Drupal\user\Plugin\views\filter\Name; /** * Defines a filter for filtering on user references. @@ -20,68 +18,52 @@ use Drupal\Core\Form\FormStateInterface; * * @ViewsFilter("search_api_user") */ -class SearchApiUser extends SearchApiFilterEntityBase { +class SearchApiUser extends Name { - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - parent::valueForm($form, $form_state); - - // Set autocompletion. - $path = $this->isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete'; - $form['value']['#autocomplete_path'] = $path; - } - - /** - * {@inheritdoc} - */ - protected function idsToString(array $ids) { - $names = array(); - $args[':uids'] = array_filter($ids); - $result = Database::getConnection()->query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args); - $result = $result->fetchAllKeyed(); - foreach ($ids as $uid) { - if (!$uid) { - $names[] = \Drupal::config('user.settings')->get('anonymous'); - } - elseif (isset($result[$uid])) { - $names[] = $result[$uid]; - } - } - return implode(', ', $names); - } + use SearchApiFilterTrait; /** * {@inheritdoc} */ - protected function validateEntityStrings(array &$form, array $values, FormStateInterface $form_state) { - $uids = array(); - $missing = array(); - foreach ($values as $value) { - if (Unicode::strtolower($value) === Unicode::strtolower(\Drupal::config('user.settings')->get('anonymous'))) { - $uids[] = 0; - } - else { - $missing[strtolower($value)] = $value; - } - } - - if (!$missing) { - return $uids; - } - - $result = Database::getConnection()->query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing))); - foreach ($result as $account) { - unset($missing[strtolower($account->name)]); - $uids[] = $account->uid; - } - - if ($missing) { - $form_state->setError($form, $this->formatPlural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing)))); - } - - return $uids; + function operators() { + return array( + 'or' => array( + 'title' => $this->t('Is one of'), + 'short' => $this->t('or'), + 'short_single' => $this->t('='), + 'method' => 'opHelper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'and' => array( + 'title' => $this->t('Is all of'), + 'short' => $this->t('and'), + 'short_single' => $this->t('='), + 'method' => 'opHelper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'not' => array( + 'title' => $this->t('Is none of'), + 'short' => $this->t('not'), + 'short_single' => $this->t('<>'), + 'method' => 'opHelper', + 'values' => 1, + 'ensure_my_table' => 'helper', + ), + 'empty' => array( + 'title' => $this->t('Is empty (NULL)'), + 'method' => 'opEmpty', + 'short' => $this->t('empty'), + 'values' => 0, + ), + 'not empty' => array( + 'title' => $this->t('Is not empty (NOT NULL)'), + 'method' => 'opEmpty', + 'short' => $this->t('not empty'), + 'values' => 0, + ), + ); } } diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index aad5797..86dd166 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\Plugin\views\query; use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Html; +use Drupal\Core\Database\Query\ConditionInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; @@ -18,6 +19,7 @@ use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; +use Drupal\views\ViewsData; /** * Defines a Views query class for searching on Search API indexes. @@ -97,7 +99,7 @@ class SearchApiQuery extends QueryPluginBase { * * @var array */ - protected $filters = array(); + protected $where = array(); /** * The conjunction with which multiple filter groups are combined. @@ -118,6 +120,9 @@ class SearchApiQuery extends QueryPluginBase { * The requested search index, or NULL if it could not be found and loaded. */ public static function getIndexFromTable($table, EntityManagerInterface $entity_manager = NULL) { + // @todo Instead use views.views_data service (ViewsData) – injected, too – + // to load the base table definition and use the "index" (or maybe + // rename to "search_api_index") field from there. if (substr($table, 0, 17) == 'search_api_index_') { $index_id = substr($table, 17); if ($entity_manager) { @@ -260,7 +265,7 @@ class SearchApiQuery extends QueryPluginBase { } // Setup the nested filter structure for this query. - if (!empty($this->filters)) { + if (!empty($this->where)) { // If the different groups are combined with the OR operator, we have to // add a new OR filter to the query to which the filters for the groups // will be added. @@ -272,7 +277,7 @@ class SearchApiQuery extends QueryPluginBase { $base = $this->query; } // Add a nested filter for each filter group, with its set conjunction. - foreach ($this->filters as $group_id => $group) { + foreach ($this->where as $group_id => $group) { if (!empty($group['conditions']) || !empty($group['filters'])) { $group += array('type' => 'AND'); // For filters without a group, we want to always add them directly to @@ -608,16 +613,21 @@ class SearchApiQuery extends QueryPluginBase { * * @param \Drupal\search_api\Query\FilterInterface $filter * A filter that should be added as a subfilter. - * @param string|null $group + * @param int $group * (optional) The Views query filter group to add this filter to. * * @return $this * * @see \Drupal\search_api\Query\QueryInterface::filter() */ - public function filter(FilterInterface $filter, $group = NULL) { + public function filter(FilterInterface $filter, $group = 0) { if (!$this->shouldAbort()) { - $this->filters[$group]['filters'][] = $filter; + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + $this->where[$group]['filters'][] = $filter; } return $this; } @@ -639,21 +649,168 @@ class SearchApiQuery extends QueryPluginBase { * respectively. * If $value is NULL, $operator also can only be "=" or "<>", meaning the * field must have no or some value, respectively. - * @param string|null $group + * @param int $group * (optional) The Views query filter group to add this filter to. * * @return $this * * @see \Drupal\search_api\Query\QueryInterface::condition() */ - public function condition($field, $value, $operator = '=', $group = NULL) { + public function condition($field, $value, $operator = '=', $group = 0) { if (!$this->shouldAbort()) { - $this->filters[$group]['conditions'][] = array($field, $value, $operator); + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all + // the default group. + if (empty($group)) { + $group = 0; + } + $this->where[$group]['conditions'][] = array($field, $value, $operator); } return $this; } /** + * Adds a simple condition to the query. + * + * This replicates the interface of Views' default SQL backend to simplify + * the Views integration of the Search API. If you are writing Search + * API-specific Views code, you should better use the filter() or condition() + * methods. + * + * @param int $group + * The condition group to add these to; groups are used to create AND/OR + * sections. Groups cannot be nested. Use 0 as the default group. + * If the group does not yet exist it will be created as an AND group. + * @param string|\Drupal\Core\Database\Query\ConditionInterface|\Drupal\search_api\Query\FilterInterface $field + * The ID of the field to check; or a filter object to add to the query; or, + * for compatibility purposes, a database condition object to transform into + * a search filter object and add to the query. If a field ID is passed and + * starts with a period (.), it will be stripped. + * @param mixed $value + * (optional) The value the field should have (or be related to by the + * operator). Or NULL if an object is passed as $field. + * @param string|null $operator + * (optional) The operator to use for checking the constraint. The following + * operators are supported for primitive types: "=", "<>", "<", "<=", ">=", + * ">". They have the same semantics as the corresponding SQL operators. + * If $field is a fulltext field, $operator can only be "=" or "<>", which + * are in this case interpreted as "contains" or "doesn't contain", + * respectively. + * If $value is NULL, $operator also can only be "=" or "<>", meaning the + * field must have no or some value, respectively. + * To stay compatible with Views, "!=" is supported as an alias for "<>". + * If an object is passed as $field, $operator should be NULL. + * + * @return $this + * + * @see \Drupal\views\Plugin\views\query\Sql::addWhere() + * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::filter() + * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::condition() + */ + public function addWhere($group, $field, $value = NULL, $operator = NULL) { + if ($this->shouldAbort()) { + return $this; + } + + // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all the + // default group. + if (empty($group)) { + $group = 0; + } + + if (is_object($field)) { + if ($field instanceof ConditionInterface) { + $field = $this->transformConditionToFilter($field); + } + if ($field instanceof FilterInterface) { + $this->where[$group]['filters'][] = $field; + } + elseif (!$this->shouldAbort()) { + // We only need to abort if that wasn't done by + // transformConditionToFilter() already. + $this->abort('Unexpected condition passed to addWhere().'); + } + } + else { + $this->where[$group]['conditions'][] = array($this->sanitizeFieldId($field), $value, $this->sanitizeOperator($operator)); + } + + return $this; + } + + /** + * Transforms a database condition to an equivalent search filter. + * + * @param \Drupal\Core\Database\Query\ConditionInterface $db_condition + * The condition to transform. + * + * @return \Drupal\search_api\Query\FilterInterface|null + * A search filter equivalent to $condition, or NULL if the transformation + * failed. + */ + protected function transformConditionToFilter(ConditionInterface $db_condition) { + $conditions = $db_condition->conditions(); + $filter = $this->query->createFilter($conditions['#conjunction']); + unset($conditions['#conjunction']); + foreach ($conditions as $condition) { + if ($condition['operator'] === NULL) { + $this->abort('Trying to include a raw SQL condition in a Search API query.'); + return NULL; + } + if ($condition['field'] instanceof ConditionInterface) { + $nested_filter = $this->transformConditionToFilter($condition['field']); + if ($nested_filter) { + $filter->filter($nested_filter); + } + else { + return NULL; + } + } + else { + $filter->condition($this->sanitizeFieldId($condition['field']), $condition['value'], $this->sanitizeOperator($condition['operator'])); + } + } + return $filter; + } + + /** + * Adapts a field ID for use in a Search API query. + * + * This method will remove a leading period (.), if present. This is done + * because in the SQL Views query plugin field IDs are always prefixed with a + * table alias (in our case always empty) followed by a period. + * + * @param string $field_id + * The field ID to adapt for use in the Search API. + * + * @return string + * The sanitized field ID. + */ + protected function sanitizeFieldId($field_id) { + if ($field_id && $field_id[0] === '.') { + $field_id = substr($field_id, 1); + } + return $field_id; + } + + /** + * Adapts an operator for use in a Search API query. + * + * This method maps Views' "!=" to the "<>" Search API uses. + * + * @param string $operator + * The operator to adapt for use in the Search API. + * + * @return string + * The sanitized operator. + */ + protected function sanitizeOperator($operator) { + if ($operator === '!=') { + $operator = '<>'; + } + return $operator; + } + + /** * Adds a sort directive to this search query. * * If no sort is manually set, the results will be sorted descending by diff --git a/src/Plugin/views/sort/SearchApiSort.php b/src/Plugin/views/sort/SearchApiSort.php index 0d8d815..3214517 100644 --- a/src/Plugin/views/sort/SearchApiSort.php +++ b/src/Plugin/views/sort/SearchApiSort.php @@ -12,7 +12,7 @@ use Drupal\views\Plugin\views\sort\SortPluginBase; /** * Provides a sort plugin for Search API views. * - * @ViewsSort("search_api_sort") + * @ViewsSort("search_api") */ class SearchApiSort extends SortPluginBase { diff --git a/src/Tests/Processor/ProcessorTestBase.php b/src/Tests/Processor/ProcessorTestBase.php index ec22ed9..e7d8fd8 100644 --- a/src/Tests/Processor/ProcessorTestBase.php +++ b/src/Tests/Processor/ProcessorTestBase.php @@ -101,8 +101,7 @@ abstract class ProcessorTestBase extends EntityUnitTestBase { ), )); - /** @var \Drupal\search_api\Processor\ProcessorPluginManager $plugin_manager */ - $plugin_manager = \Drupal::service('plugin.manager.search_api.processor'); + $plugin_manager = Utility::getProcessorPluginManager(); $this->processor = $plugin_manager->createInstance($processor, array('index' => $this->index)); } $this->index->save(); diff --git a/src/Utility.php b/src/Utility.php index b5f75c6..89524b3 100644 --- a/src/Utility.php +++ b/src/Utility.php @@ -158,7 +158,7 @@ class Utility { } static::$dataTypeFallbackMapping[$index_id] = array(); /** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */ - foreach (\Drupal::service('plugin.manager.search_api.data_type')->getInstances() as $type_id => $data_type) { + foreach (self::getDataTypePluginManager()->getInstances() as $type_id => $data_type) { // We know for sure that we do not need to fall back for the default // data types as they are always present and are required to be // supported by all backends. @@ -561,4 +561,54 @@ class Utility { return array(substr($combined_id, 0, $pos), substr($combined_id, $pos + 1)); } + /** + * Retrieves the plugin manager for Search API backend plugins. + * + * @return \Drupal\search_api\Backend\BackendPluginManager + * The backend plugin manager. + */ + public static function getBackendPluginManager() { + return \Drupal::service('plugin.manager.search_api.backend'); + } + + /** + * Retrieves the plugin manager for Search API data type plugins. + * + * @return \Drupal\search_api\DataType\DataTypePluginManager + * The data type plugin manager. + */ + public static function getDataTypePluginManager() { + return \Drupal::service('plugin.manager.search_api.data_type'); + } + + /** + * Retrieves the plugin manager for Search API datasource plugins. + * + * @return \Drupal\search_api\Datasource\DatasourcePluginManager + * The datasource plugin manager. + */ + public static function getDatasourcePluginManager() { + return \Drupal::service('plugin.manager.search_api.datasource'); + } + + /** + * Retrieves the plugin manager for Search API processor plugins. + * + * @return \Drupal\search_api\Processor\ProcessorPluginManager + * The processor plugin manager. + */ + public static function getProcessorPluginManager() { + return \Drupal::service('plugin.manager.search_api.processor'); + } + + /** + * Retrieves the plugin manager for Search API tracker plugins. + * + * @return \Drupal\search_api\Tracker\TrackerPluginManager + * The tracker plugin manager. + */ + public static function getTrackerPluginManager() { + return \Drupal::service('plugin.manager.search_api.tracker'); + } + } diff --git a/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml b/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml index fdae0c8..aa03777 100644 --- a/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml +++ b/tests/search_api_test_views/config/install/views.view.search_api_test_views_fulltext.yml @@ -130,7 +130,7 @@ display: relationship: none group_type: group admin_label: '' - operator: AND + operator: and value: '' group: 1 exposed: true