Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
NumberRange
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 getTotalRanges
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 defineOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 buildOptionsForm
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 query
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
8
 postExecute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Drupal\visitors\Plugin\views\sort;
4
5use Drupal\Core\Form\FormStateInterface;
6use Drupal\views\Attribute\ViewsSort;
7use Drupal\views\Plugin\views\sort\SortPluginBase;
8use Drupal\visitors\Service\SequenceService;
9
10/**
11 * Sort handler that uses defined number ranges instead of raw values.
12 */
13#[ViewsSort("visitors_number_range")]
14class NumberRange extends SortPluginBase {
15
16  /**
17   * Gets the total count of configured ranges.
18   *
19   * @return int
20   *   The total number of ranges.
21   */
22  public function getTotalRanges(): int {
23    $ranges_text = $this->options['ranges'];
24    $ranges = preg_split('/\r\n|\r|\n/', $ranges_text);
25    $ranges = array_map('trim', $ranges);
26    $ranges = array_filter($ranges, function ($range) {
27      return trim($range) !== '';
28    });
29
30    return count($ranges);
31  }
32
33  /**
34   * {@inheritdoc}
35   */
36  public function defineOptions() {
37    $options = parent::defineOptions();
38    $options['ranges'] = ['default' => "0\n1\n2\n3\n4\n5\n6-7\n8-10\n11-14\n15-20\n21+"];
39    return $options;
40  }
41
42  /**
43   * {@inheritdoc}
44   */
45  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
46    parent::buildOptionsForm($form, $form_state);
47
48    $form['ranges'] = [
49      '#type' => 'textarea',
50      '#title' => $this->t('Number ranges'),
51      '#description' => $this->t("Enter one range per line. Examples: <code>0</code>, <code>6-7</code>, <code>21+</code>. The sort order will follow the order you define here."),
52      '#default_value' => $this->options['ranges'],
53    ];
54  }
55
56  /**
57   * {@inheritdoc}
58   */
59  public function query() {
60    // Add the base field to the query.
61    $this->ensureMyTable();
62
63    // Check if the field already exists in the query.
64    $existing_field_alias = $this->tableAlias . '_' . $this->realField . '__range';
65
66    // Look through the query fields to see if our target field already exists.
67    foreach ($this->query->fields as $field_info) {
68      if (isset($field_info['alias']) && $field_info['alias'] === $existing_field_alias) {
69        // Field already exists, use it for sorting.
70        $this->query->addOrderBy(NULL, NULL, $this->options['order'], $existing_field_alias);
71        return;
72      }
73    }
74
75    // Field doesn't exist, create our own CASE expression.
76    $field = "$this->tableAlias.$this->realField";
77
78    $ranges_text = $this->options['ranges'];
79    $ranges = preg_split('/\r\n|\r|\n/', $ranges_text);
80    $ranges = array_map('trim', $ranges);
81    $ranges = array_filter($ranges, function ($range) {
82      return trim($range) !== '';
83    });
84
85    $case_sql = [];
86    $i = 0;
87    foreach ($ranges as $range) {
88      // Range "X-Y".
89      if (preg_match('/^(\d+)\-(\d+)$/', $range, $m)) {
90        $case_sql[] = "WHEN $field BETWEEN {$m[1]} AND {$m[2]} THEN $i";
91      }
92      // Range "X+".
93      elseif (preg_match('/^(\d+)\+$/', $range, $m)) {
94        $case_sql[] = "WHEN $field >= {$m[1]} THEN $i";
95      }
96      // Exact number.
97      elseif (is_numeric($range)) {
98        $case_sql[] = "WHEN $field = {$range} THEN $i";
99      }
100    }
101
102    $case_expression = "CASE " . implode(' ', $case_sql) . " ELSE 9999 END";
103
104    // Tell Views how to sort.
105    $this->query->addOrderBy(NULL, $case_expression, $this->options['order'], $existing_field_alias);
106  }
107
108  /**
109   * {@inheritdoc}
110   */
111  public function postExecute(&$values) {
112    $values = SequenceService::integer($values, $this->tableAlias . '_' . $this->realField . '__range', $this->getTotalRanges());
113  }
114
115}