Loading core/modules/views/src/Plugin/views/filter/BooleanOperator.php +100 −15 Original line number Diff line number Diff line Loading @@ -82,7 +82,7 @@ public function operatorOptions($which = 'title') { * {@inheritdoc} */ public function operators() { return [ $operators = [ '=' => [ 'title' => $this->t('Is equal to'), 'method' => 'queryOpBoolean', Loading @@ -98,6 +98,25 @@ public function operators() { 'query_operator' => self::NOT_EQUAL, ], ]; // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ 'title' => $this->t('Is empty (NULL)'), 'method' => 'opEmpty', 'short' => $this->t('empty'), 'values' => 0, ], 'not empty' => [ 'title' => $this->t('Is not empty (NOT NULL)'), 'method' => 'opEmpty', 'short' => $this->t('not empty'), 'values' => 0, ], ]; } return $operators; } /** Loading @@ -122,6 +141,8 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ $this->accept_null = (bool) $this->definition['accept_null']; } $this->valueOptions = NULL; $this->definition['allow empty'] = TRUE; } /** Loading Loading @@ -166,6 +187,8 @@ protected function defineOptions() { } protected function valueForm(&$form, FormStateInterface $form_state) { $form['value'] = []; if (empty($this->valueOptions)) { // Initialize the array of possible values for this filter. $this->getValueOptions(); Loading @@ -178,16 +201,28 @@ protected function valueForm(&$form, FormStateInterface $form_state) { // Configuring a filter: use radios for clarity. $filter_form_type = 'radios'; } $display_options = 'all'; $source = ':input[name="options[operator]"]'; if ($exposed) { $identifier = $this->options['expose']['identifier']; if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { $display_options = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; } else { $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; } } if ($display_options === 'all' || $display_options === 'value') { $form['value'] = [ '#type' => $filter_form_type, '#title' => $this->value_value, '#options' => $this->valueOptions, '#default_value' => $this->value, ]; if (!empty($this->options['exposed'])) { $identifier = $this->options['expose']['identifier']; $user_input = $form_state->getUserInput(); if ($exposed && !isset($user_input[$identifier])) { if ($exposed && isset($identifier) && !isset($user_input[$identifier])) { $user_input[$identifier] = $this->value; $form_state->setUserInput($user_input); } Loading @@ -195,6 +230,15 @@ protected function valueForm(&$form, FormStateInterface $form_state) { if (!$exposed || empty($this->options['expose']['required'])) { $form['value']['#options'] = ['All' => $this->t('- Any -')] + $form['value']['#options']; } if ($display_options === 'all') { // Setup #states for operators with a value. foreach ($this->operatorValues(1) as $operator) { $form['value']['#states']['visible'][] = [ $source => ['value' => $operator], ]; } } } } Loading @@ -214,6 +258,8 @@ public function adminSummary() { if (empty($this->valueOptions)) { $this->getValueOptions(); } if (in_array($this->operator, $this->operatorValues(1), TRUE)) { $this->getValueOptions(); // Now that we have the valid options for this filter, just return the // human-readable label based on the current value. The valueOptions // array is keyed with either 0 or 1, so if the current value is not Loading @@ -221,6 +267,9 @@ public function adminSummary() { return $this->operator . ' ' . $this->valueOptions[!empty($this->value)]; } return $this->operator; } public function defaultExposeOptions() { parent::defaultExposeOptions(); $this->options['expose']['operator_id'] = ''; Loading @@ -237,7 +286,7 @@ public function query() { $info = $this->operators(); if (!empty($info[$this->operator]['method'])) { call_user_func([$this, $info[$this->operator]['method']], $field, $info[$this->operator]['query_operator']); $this->{$info[$this->operator]['method']}($field, $info[$this->operator]['query_operator'] ?? NULL); } } Loading Loading @@ -286,4 +335,40 @@ protected function queryOpBoolean($field, $query_operator = self::EQUAL) { } } /** * Filters by operator empty. * * @param string $field * The views field. */ protected function opEmpty(string $field): void { if ($this->operator === 'empty') { $operator = "IS NULL"; } else { $operator = "IS NOT NULL"; } $this->query->addWhere($this->options['group'], $field, NULL, $operator); } /** * Returns operators for values. * * @param int $values * The values filter value. * * @return string[] * A filtered list of operators. */ protected function operatorValues(int $values = 1): array { $options = []; foreach ($this->operators() as $id => $info) { if (isset($info['values']) && $info['values'] === $values) { $options[] = $id; } } return $options; } } core/modules/views/tests/src/Kernel/Handler/FilterBooleanOperatorTest.php +77 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,30 @@ class FilterBooleanOperatorTest extends ViewsKernelTestBase { 'views_test_data_id' => 'id', ]; /** * {@inheritdoc} */ protected function dataSet() { $dataset = parent::dataSet(); $dataset[] = [ 'name' => 'Null', 'age' => 0, 'job' => 'Null', 'created' => 0, 'status' => NULL, ]; return $dataset; } /** * {@inheritdoc} */ protected function schemaDefinition() { $schema = parent::schemaDefinition(); $schema['views_test_data']['fields']['status']['not null'] = FALSE; return $schema; } /** * Tests the BooleanOperator filter. */ Loading Loading @@ -112,6 +136,59 @@ public function testFilterBooleanOperator(): void { $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); } /** * Tests the BooleanOperator empty/not empty filters. */ public function testEmptyFilterBooleanOperator(): void { $view = Views::getView('test_view'); $view->setDisplay(); // Add an "empty" boolean filter on status. $view->displayHandlers->get('default')->overrideOption('filters', [ 'status' => [ 'id' => 'status', 'field' => 'status', 'table' => 'views_test_data', 'operator' => 'empty', ], ]); $this->executeView($view); $expected_result = [ ['id' => 6], ]; $this->assertCount(1, $view->result); $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); $view->destroy(); $view->setDisplay(); // Add a "not empty" boolean filter on status. $view->displayHandlers->get('default')->overrideOption('filters', [ 'status' => [ 'id' => 'status', 'field' => 'status', 'table' => 'views_test_data', 'operator' => 'not empty', ], ]); $this->executeView($view); $expected_result = [ ['id' => 1], ['id' => 2], ['id' => 3], ['id' => 4], ['id' => 5], ]; $this->assertCount(5, $view->result); $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); $view->destroy(); } /** * Tests the boolean filter with grouped exposed form enabled. */ Loading Loading
core/modules/views/src/Plugin/views/filter/BooleanOperator.php +100 −15 Original line number Diff line number Diff line Loading @@ -82,7 +82,7 @@ public function operatorOptions($which = 'title') { * {@inheritdoc} */ public function operators() { return [ $operators = [ '=' => [ 'title' => $this->t('Is equal to'), 'method' => 'queryOpBoolean', Loading @@ -98,6 +98,25 @@ public function operators() { 'query_operator' => self::NOT_EQUAL, ], ]; // If the definition allows for the empty operator, add it. if (!empty($this->definition['allow empty'])) { $operators += [ 'empty' => [ 'title' => $this->t('Is empty (NULL)'), 'method' => 'opEmpty', 'short' => $this->t('empty'), 'values' => 0, ], 'not empty' => [ 'title' => $this->t('Is not empty (NOT NULL)'), 'method' => 'opEmpty', 'short' => $this->t('not empty'), 'values' => 0, ], ]; } return $operators; } /** Loading @@ -122,6 +141,8 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ $this->accept_null = (bool) $this->definition['accept_null']; } $this->valueOptions = NULL; $this->definition['allow empty'] = TRUE; } /** Loading Loading @@ -166,6 +187,8 @@ protected function defineOptions() { } protected function valueForm(&$form, FormStateInterface $form_state) { $form['value'] = []; if (empty($this->valueOptions)) { // Initialize the array of possible values for this filter. $this->getValueOptions(); Loading @@ -178,16 +201,28 @@ protected function valueForm(&$form, FormStateInterface $form_state) { // Configuring a filter: use radios for clarity. $filter_form_type = 'radios'; } $display_options = 'all'; $source = ':input[name="options[operator]"]'; if ($exposed) { $identifier = $this->options['expose']['identifier']; if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) { $display_options = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none'; } else { $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]'; } } if ($display_options === 'all' || $display_options === 'value') { $form['value'] = [ '#type' => $filter_form_type, '#title' => $this->value_value, '#options' => $this->valueOptions, '#default_value' => $this->value, ]; if (!empty($this->options['exposed'])) { $identifier = $this->options['expose']['identifier']; $user_input = $form_state->getUserInput(); if ($exposed && !isset($user_input[$identifier])) { if ($exposed && isset($identifier) && !isset($user_input[$identifier])) { $user_input[$identifier] = $this->value; $form_state->setUserInput($user_input); } Loading @@ -195,6 +230,15 @@ protected function valueForm(&$form, FormStateInterface $form_state) { if (!$exposed || empty($this->options['expose']['required'])) { $form['value']['#options'] = ['All' => $this->t('- Any -')] + $form['value']['#options']; } if ($display_options === 'all') { // Setup #states for operators with a value. foreach ($this->operatorValues(1) as $operator) { $form['value']['#states']['visible'][] = [ $source => ['value' => $operator], ]; } } } } Loading @@ -214,6 +258,8 @@ public function adminSummary() { if (empty($this->valueOptions)) { $this->getValueOptions(); } if (in_array($this->operator, $this->operatorValues(1), TRUE)) { $this->getValueOptions(); // Now that we have the valid options for this filter, just return the // human-readable label based on the current value. The valueOptions // array is keyed with either 0 or 1, so if the current value is not Loading @@ -221,6 +267,9 @@ public function adminSummary() { return $this->operator . ' ' . $this->valueOptions[!empty($this->value)]; } return $this->operator; } public function defaultExposeOptions() { parent::defaultExposeOptions(); $this->options['expose']['operator_id'] = ''; Loading @@ -237,7 +286,7 @@ public function query() { $info = $this->operators(); if (!empty($info[$this->operator]['method'])) { call_user_func([$this, $info[$this->operator]['method']], $field, $info[$this->operator]['query_operator']); $this->{$info[$this->operator]['method']}($field, $info[$this->operator]['query_operator'] ?? NULL); } } Loading Loading @@ -286,4 +335,40 @@ protected function queryOpBoolean($field, $query_operator = self::EQUAL) { } } /** * Filters by operator empty. * * @param string $field * The views field. */ protected function opEmpty(string $field): void { if ($this->operator === 'empty') { $operator = "IS NULL"; } else { $operator = "IS NOT NULL"; } $this->query->addWhere($this->options['group'], $field, NULL, $operator); } /** * Returns operators for values. * * @param int $values * The values filter value. * * @return string[] * A filtered list of operators. */ protected function operatorValues(int $values = 1): array { $options = []; foreach ($this->operators() as $id => $info) { if (isset($info['values']) && $info['values'] === $values) { $options[] = $id; } } return $options; } }
core/modules/views/tests/src/Kernel/Handler/FilterBooleanOperatorTest.php +77 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,30 @@ class FilterBooleanOperatorTest extends ViewsKernelTestBase { 'views_test_data_id' => 'id', ]; /** * {@inheritdoc} */ protected function dataSet() { $dataset = parent::dataSet(); $dataset[] = [ 'name' => 'Null', 'age' => 0, 'job' => 'Null', 'created' => 0, 'status' => NULL, ]; return $dataset; } /** * {@inheritdoc} */ protected function schemaDefinition() { $schema = parent::schemaDefinition(); $schema['views_test_data']['fields']['status']['not null'] = FALSE; return $schema; } /** * Tests the BooleanOperator filter. */ Loading Loading @@ -112,6 +136,59 @@ public function testFilterBooleanOperator(): void { $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); } /** * Tests the BooleanOperator empty/not empty filters. */ public function testEmptyFilterBooleanOperator(): void { $view = Views::getView('test_view'); $view->setDisplay(); // Add an "empty" boolean filter on status. $view->displayHandlers->get('default')->overrideOption('filters', [ 'status' => [ 'id' => 'status', 'field' => 'status', 'table' => 'views_test_data', 'operator' => 'empty', ], ]); $this->executeView($view); $expected_result = [ ['id' => 6], ]; $this->assertCount(1, $view->result); $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); $view->destroy(); $view->setDisplay(); // Add a "not empty" boolean filter on status. $view->displayHandlers->get('default')->overrideOption('filters', [ 'status' => [ 'id' => 'status', 'field' => 'status', 'table' => 'views_test_data', 'operator' => 'not empty', ], ]); $this->executeView($view); $expected_result = [ ['id' => 1], ['id' => 2], ['id' => 3], ['id' => 4], ['id' => 5], ]; $this->assertCount(5, $view->result); $this->assertIdenticalResultset($view, $expected_result, $this->columnMap); $view->destroy(); } /** * Tests the boolean filter with grouped exposed form enabled. */ Loading