diff --git a/core/modules/views/src/Plugin/views/filter/GroupByString.php b/core/modules/views/src/Plugin/views/filter/GroupByString.php new file mode 100644 index 0000000000..ce2eb8aa96 --- /dev/null +++ b/core/modules/views/src/Plugin/views/filter/GroupByString.php @@ -0,0 +1,212 @@ +ensureMyTable(); + $field = $this->getField(); + + $info = $this->operators(); + if (!empty($info[$this->operator]['method'])) { + $this->{$info[$this->operator]['method']}($field); + } + } + + /** + * {@inheritdoc} + */ + public function opEqual($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field $this->operator $placeholder", [ + $placeholder => $this->value, + ]); + } + + /** + * {@inheritdoc} + */ + protected function opContains($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field LIKE $placeholder", [ + $placeholder => '%' . db_like($this->value) . '%', + ]); + } + + /** + * {@inheritdoc} + */ + protected function opContainsWord($field) { + // Don't filter on empty strings. + if (empty($this->value)) { + return; + } + + $haystack = []; + + preg_match_all(static::WORDS_PATTERN, ' ' . $this->value, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $phrase = FALSE; + // Strip off phrase quotes. + if ($match[2][0] == '"') { + $match[2] = substr($match[2], 1, -1); + $phrase = TRUE; + } + $words = trim($match[2], ',?!();:-'); + $words = $phrase ? [$words] : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY); + + foreach ($words as $word) { + $haystack[] = $word; + } + } + + if (empty($haystack)) { + return; + } + + if ($this->operator == 'word') { + $placeholder = $this->placeholder() . '[]'; + $this->query->addHavingExpression($this->options['group'], "$field IN ($placeholder)", [ + $placeholder => $haystack, + ]); + } + else { + $snippet = []; + + foreach ($haystack as $word) { + $snippet[] = "$field LIKE '%" . db_like($word) . "%'"; + } + + $this->query->addHavingExpression($this->options['group'], implode(') AND (', $snippet)); + } + } + + /** + * {@inheritdoc} + */ + protected function opStartsWith($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field LIKE $placeholder", [ + $placeholder => db_like($this->value) . '%', + ]); + } + + /** + * {@inheritdoc} + */ + protected function opNotStartsWith($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field NOT LIKE $placeholder", [ + $placeholder => db_like($this->value) . '%', + ]); + } + + /** + * {@inheritdoc} + */ + protected function opEndsWith($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field LIKE $placeholder", [ + $placeholder => '%' . db_like($this->value), + ]); + } + + /** + * {@inheritdoc} + */ + protected function opNotEndsWith($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field NOT LIKE $placeholder", [ + $placeholder => '%' . db_like($this->value), + ]); + } + + /** + * {@inheritdoc} + */ + protected function opNotLike($field) { + $placeholder = $this->placeholder(); + $this->query->addHavingExpression($this->options['group'], "$field NOT LIKE $placeholder", [ + $placeholder => '%' . db_like($this->value) . '%', + ]); + } + + /** + * {@inheritdoc} + */ + protected function opShorterThan($field) { + $placeholder = $this->placeholder(); + // Type cast the argument to an integer because the SQLite database driver + // has to do some specific alterations to the query base on that data type. + $this->query->addHavingExpression($this->options['group'], "LENGTH($field) < $placeholder", [ + $placeholder => (int) $this->value, + ]); + } + + /** + * {@inheritdoc} + */ + protected function opLongerThan($field) { + $placeholder = $this->placeholder(); + // Type cast the argument to an integer because the SQLite database driver + // has to do some specific alterations to the query base on that data type. + $this->query->addHavingExpression($this->options['group'], "LENGTH($field) > $placeholder", [ + $placeholder => (int) $this->value, + ]); + } + + /** + * Filters by a regular expression. + * + * @param string $field + * The expression pointing to the queries field, for example "foo.bar". + */ + protected function opRegex($field) { + $placeholder = $this->placeholder(); + // Type cast the argument to an integer because the SQLite database driver + // has to do some specific alterations to the query base on that data type. + $this->query->addHavingExpression($this->options['group'], "$field REGEXP $placeholder", [ + $placeholder => $this->value, + ]); + } + + /** + * {@inheritdoc} + */ + protected function opEmpty($field) { + if ($this->operator == 'empty') { + $operator = "IS NULL"; + } + else { + $operator = "IS NOT NULL"; + } + + $this->query->addHavingExpression($this->options['group'], "$field $operator"); + } + + /** + * {@inheritdoc} + */ + public function adminLabel($short = FALSE) { + return $this->getField(parent::adminLabel($short)); + } + + /** + * {@inheritdoc} + */ + public function canGroup() { + return FALSE; + } + +} diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 96968b5e32..002096438d 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -190,7 +190,7 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o 'base' => $base_table, ]; - // init the table queue with our primary table. + // Init the table queue with our primary table. $this->tableQueue[$base_table] = [ 'alias' => $base_table, 'table' => $base_table, @@ -198,7 +198,7 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o 'join' => NULL, ]; - // init the tables with our primary table + // Init the tables with our primary table. $this->tables[$base_table][$base_table] = [ 'count' => 1, 'alias' => $base_table, @@ -260,6 +260,9 @@ public function setCountField($table, $field, $alias = NULL) { ]; } + /** + * {@inheritdoc} + */ protected function defineOptions() { $options = parent::defineOptions(); $options['disable_sql_rewrite'] = [ @@ -535,13 +538,16 @@ public function queueTable($table, $relationship = NULL, JoinPluginBase $join = return $alias; } + /** + * {@inheritdoc} + */ protected function markTable($table, $relationship, $alias) { // Mark that this table has been added. if (empty($this->tables[$relationship][$table])) { if (!isset($alias)) { $alias = ''; if ($relationship != $this->view->storage->get('base_table')) { - // double underscore will help prevent accidental name + // Double underscore will help prevent accidental name // space collisions. $alias = $relationship . '__'; } @@ -578,7 +584,7 @@ protected function markTable($table, $relationship, $alias) { * cannot be ensured. */ public function ensureTable($table, $relationship = NULL, JoinPluginBase $join = NULL) { - // ensure a relationship + // Ensure a relationship. if (empty($relationship)) { $relationship = $this->view->storage->get('base_table'); } @@ -627,11 +633,9 @@ public function ensureTable($table, $relationship = NULL, JoinPluginBase $join = // the same table with the same join multiple times. For // example, a view that filters on 3 taxonomy terms using AND // needs to join taxonomy_term_data 3 times with the same join. - - // scan through the table queue to see if a matching join and + // Scan through the table queue to see if a matching join and // relationship exists. If so, use it instead of this join. - - // TODO: Scanning through $this->tableQueue results in an + // @todo Scanning through $this->tableQueue results in an // O(N^2) algorithm, and this code runs every time the view is // instantiated (Views 2 does not currently cache queries). // There are a couple possible "improvements" but we should do @@ -721,7 +725,6 @@ protected function adjustJoin($join, $relationship) { if ($relationship != $this->view->storage->get('base_table')) { // If we're linking to the primary table, the relationship to use will // be the prior relationship. Unless it's a direct link. - // Safety! Don't modify an original here. $join = clone $join; @@ -732,7 +735,7 @@ protected function adjustJoin($join, $relationship) { $this->ensureTable($join->leftTable, $relationship); } - // First, if this is our link point/anchor table, just use the relationship + // First, if this is our link point/anchor table, just use the relationship. if ($join->leftTable == $this->relationships[$relationship]['table']) { $join->leftTable = $relationship; } @@ -830,14 +833,13 @@ public function addField($table, $field, $alias = '', $params = []) { $alias = $table . '_' . $field; } - // Make sure an alias is assigned + // Make sure an alias is assigned. $alias = $alias ? $alias : $field; // PostgreSQL truncates aliases to 63 characters: // https://www.drupal.org/node/571548. - // We limit the length of the original alias up to 60 characters - // to get a unique alias later if its have duplicates + // to get a unique alias later if its have duplicates. $alias = strtolower(substr($alias, 0, 60)); // Create a field info array. @@ -1024,7 +1026,7 @@ public function addHavingExpression($group, $snippet, $args = []) { */ public function addOrderBy($table, $field = NULL, $order = 'ASC', $alias = '', $params = []) { // Only ensure the table if it's not the special random key. - // @todo: Maybe it would make sense to just add an addOrderByRand or something similar. + // @todo Maybe it would make sense to just add an addOrderByRand or something similar. if ($table && $table != 'rand') { $this->ensureTable($table); } @@ -1377,7 +1379,7 @@ public function query($get_count = FALSE) { } if (!$this->getCountOptimized) { - // we only add the orderby if we're not counting. + // We only add the orderby if we're not counting. if ($this->orderby) { foreach ($this->orderby as $order) { if ($order['field'] == 'rand_') { @@ -1717,12 +1719,18 @@ protected function getAllEntities() { return $entities; } + /** + * {@inheritdoc} + */ public function addSignature(ViewExecutable $view) { $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name'); } + /** + * {@inheritdoc} + */ public function getAggregationInfo() { - // @todo -- need a way to get database specific and customized aggregation + // @todo need a way to get database specific and customized aggregation // functions into here. return [ 'group' => [ @@ -1799,13 +1807,39 @@ public function getAggregationInfo() { 'sort' => 'groupby_numeric', ], ], + 'group_concat' => [ + 'title' => $this->t('Group Concat'), + 'method' => 'aggregationMethodSimple', + 'handler' => [ + 'argument' => 'groupby_numeric', + 'field' => 'standard', + 'filter' => 'groupby_string', + 'sort' => 'groupby_numeric', + ], + ], + 'group_concat_distinct' => [ + 'title' => $this->t('Group Concat DISTINCT'), + 'method' => 'aggregationMethodDistinct', + 'handler' => [ + 'argument' => 'groupby_numeric', + 'field' => 'standard', + 'filter' => 'groupby_string', + 'sort' => 'groupby_numeric', + ], + ], ]; } + /** + * {@inheritdoc} + */ public function aggregationMethodSimple($group_type, $field) { return strtoupper($group_type) . '(' . $field . ')'; } + /** + * {@inheritdoc} + */ public function aggregationMethodDistinct($group_type, $field) { $group_type = str_replace('_distinct', '', $group_type); return strtoupper($group_type) . '(DISTINCT ' . $field . ')';