ManyToOneHelper.php 11.9 KB
Newer Older
dawehner's avatar
dawehner committed
1
2
<?php

aspilicious's avatar
aspilicious committed
3
4
5
6
7
/**
 * @file
 * Definition of Drupal\views\ManyToOneHelper.
 */

dawehner's avatar
dawehner committed
8
9
namespace Drupal\views;

10
11
use Drupal\views\Plugin\views\HandlerBase;

dawehner's avatar
dawehner committed
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * This many to one helper object is used on both arguments and filters.
 *
 * @todo This requires extensive documentation on how this class is to
 * be used. For now, look at the arguments and filters that use it. Lots
 * of stuff is just pass-through but there are definitely some interesting
 * areas where they interact.
 *
 * Any handler that uses this can have the following possibly additional
 * definition terms:
 * - numeric: If true, treat this field as numeric, using %d instead of %s in
 *            queries.
 *
 */
class ManyToOneHelper {

  function __construct($handler) {
    $this->handler = $handler;
  }

32
  public static function defineOptions(&$options) {
dawehner's avatar
dawehner committed
33
34
35
    $options['reduce_duplicates'] = array('default' => FALSE, 'bool' => TRUE);
  }

36
  public function buildOptionsForm(&$form, &$form_state) {
dawehner's avatar
dawehner committed
37
38
39
    $form['reduce_duplicates'] = array(
      '#type' => 'checkbox',
      '#title' => t('Reduce duplicates'),
40
      '#description' => t("This filter can cause items that have more than one of the selected options to appear as duplicate results. If this filter causes duplicate results to occur, this checkbox can reduce those duplicates; however, the more terms it has to search for, the less performant the query will be, so use this with caution. Shouldn't be set on single-value fields, as it may cause values to disappear from display, if used on an incompatible field."),
dawehner's avatar
dawehner committed
41
42
43
44
45
46
47
48
      '#default_value' => !empty($this->handler->options['reduce_duplicates']),
      '#weight' => 4,
    );
  }

  /**
   * Sometimes the handler might want us to use some kind of formula, so give
   * it that option. If it wants us to do this, it must set $helper->formula = TRUE
49
   * and implement handler->getFormula();
dawehner's avatar
dawehner committed
50
   */
51
  public function getField() {
dawehner's avatar
dawehner committed
52
    if (!empty($this->formula)) {
53
      return $this->handler->getFormula();
dawehner's avatar
dawehner committed
54
55
    }
    else {
56
      return $this->handler->tableAlias . '.' . $this->handler->realField;
dawehner's avatar
dawehner committed
57
58
59
60
61
62
63
64
65
66
67
    }
  }

  /**
   * Add a table to the query.
   *
   * This is an advanced concept; not only does it add a new instance of the table,
   * but it follows the relationship path all the way down to the relationship
   * link point and adds *that* as a new relationship and then adds the table to
   * the relationship, if necessary.
   */
68
  public function addTable($join = NULL, $alias = NULL) {
dawehner's avatar
dawehner committed
69
70
71
72
    // This is used for lookups in the many_to_one table.
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;

    if (empty($join)) {
73
      $join = $this->getJoin();
dawehner's avatar
dawehner committed
74
75
76
77
78
79
80
81
    }

    // See if there's a chain between us and the base relationship. If so, we need
    // to create a new relationship to use.
    $relationship = $this->handler->relationship;

    // Determine the primary table to seek
    if (empty($this->handler->query->relationships[$relationship])) {
82
      $base_table = $this->handler->view->storage->get('base_table');
dawehner's avatar
dawehner committed
83
84
85
86
87
88
    }
    else {
      $base_table = $this->handler->query->relationships[$relationship]['base'];
    }

    // Cycle through the joins. This isn't as error-safe as the normal
89
    // ensurePath logic. Perhaps it should be.
dawehner's avatar
dawehner committed
90
    $r_join = clone $join;
91
92
    while ($r_join->leftTable != $base_table) {
      $r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
dawehner's avatar
dawehner committed
93
94
95
    }
    // If we found that there are tables in between, add the relationship.
    if ($r_join->table != $join->table) {
96
      $relationship = $this->handler->query->addRelationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
dawehner's avatar
dawehner committed
97
98
99
    }

    // And now add our table, using the new relationship if one was used.
100
    $alias = $this->handler->query->addTable($this->handler->table, $relationship, $join, $alias);
dawehner's avatar
dawehner committed
101
102
103
104
105
106
107
108
109
110
111
112
113

    // Store what values are used by this table chain so that other chains can
    // automatically discard those values.
    if (empty($this->handler->view->many_to_one_tables[$field])) {
      $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
    }
    else {
      $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
    }

    return $alias;
  }

114
115
  public function getJoin() {
    return $this->handler->getJoin();
dawehner's avatar
dawehner committed
116
117
118
119
120
121
  }

  /**
   * Provide the proper join for summary queries. This is important in part because
   * it will cooperate with other arguments if possible.
   */
122
  public function summaryJoin() {
dawehner's avatar
dawehner committed
123
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
124
    $join = $this->getJoin();
dawehner's avatar
dawehner committed
125
126
127
128
129
130
131
132
133
134
135

    // shortcuts
    $options = $this->handler->options;
    $view = &$this->handler->view;
    $query = &$this->handler->query;

    if (!empty($options['require_value'])) {
      $join->type = 'INNER';
    }

    if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
136
      return $query->ensureTable($this->handler->table, $this->handler->relationship, $join);
dawehner's avatar
dawehner committed
137
138
139
140
141
142
    }
    else {
      if (!empty($view->many_to_one_tables[$field])) {
        foreach ($view->many_to_one_tables[$field] as $value) {
          $join->extra = array(
            array(
143
              'field' => $this->handler->realField,
dawehner's avatar
dawehner committed
144
145
146
147
148
149
150
              'operator' => '!=',
              'value' => $value,
              'numeric' => !empty($this->definition['numeric']),
            ),
          );
        }
      }
151
      return $this->addTable($join);
dawehner's avatar
dawehner committed
152
153
154
155
    }
  }

  /**
156
   * Override ensureMyTable so we can control how this joins in.
dawehner's avatar
dawehner committed
157
158
   * The operator actually has influence over joining.
   */
159
  public function ensureMyTable() {
160
    if (!isset($this->handler->tableAlias)) {
dawehner's avatar
dawehner committed
161
162
163
164
165
166
167
      // Case 1: Operator is an 'or' and we're not reducing duplicates.
      // We hence get the absolute simplest:
      $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
      if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
        if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
          // query optimization, INNER joins are slightly faster, so use them
          // when we know we can.
168
          $join = $this->getJoin();
dawehner's avatar
dawehner committed
169
170
171
          if (isset($join)) {
            $join->type = 'INNER';
          }
172
          $this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
dawehner's avatar
dawehner committed
173
174
175
          $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
        }
        else {
176
          $join = $this->getJoin();
dawehner's avatar
dawehner committed
177
178
179
180
181
          $join->type = 'LEFT';
          if (!empty($this->handler->view->many_to_one_tables[$field])) {
            foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
              $join->extra = array(
                array(
182
                  'field' => $this->handler->realField,
dawehner's avatar
dawehner committed
183
184
185
186
187
188
189
190
                  'operator' => '!=',
                  'value' => $value,
                  'numeric' => !empty($this->handler->definition['numeric']),
                ),
              );
            }
          }

191
          $this->handler->tableAlias = $this->addTable($join);
dawehner's avatar
dawehner committed
192
193
        }

194
        return $this->handler->tableAlias;
dawehner's avatar
dawehner committed
195
196
197
198
199
200
      }

      // Case 2: it's an 'and' or an 'or'.
      // We do one join per selected value.
      if ($this->handler->operator != 'not') {
        // Clone the join for each table:
201
        $this->handler->tableAliases = array();
dawehner's avatar
dawehner committed
202
        foreach ($this->handler->value as $value) {
203
          $join = $this->getJoin();
dawehner's avatar
dawehner committed
204
205
206
207
208
          if ($this->handler->operator == 'and') {
            $join->type = 'INNER';
          }
          $join->extra = array(
            array(
209
              'field' => $this->handler->realField,
dawehner's avatar
dawehner committed
210
211
212
213
214
215
216
217
218
219
220
221
222
              'value' => $value,
              'numeric' => !empty($this->handler->definition['numeric']),
            ),
          );

          // The table alias needs to be unique to this value across the
          // multiple times the filter or argument is called by the view.
          if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
            if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
              $this->handler->view->many_to_one_count[$this->handler->table] = 0;
            }
            $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
          }
223
          $alias = $this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
dawehner's avatar
dawehner committed
224
225

          // and set table_alias to the first of these.
226
227
          if (empty($this->handler->tableAlias)) {
            $this->handler->tableAlias = $alias;
dawehner's avatar
dawehner committed
228
229
230
231
232
233
234
          }
        }
      }
      // Case 3: it's a 'not'.
      // We just do one join. We'll add a where clause during
      // the query phase to ensure that $table.$field IS NULL.
      else {
235
        $join = $this->getJoin();
dawehner's avatar
dawehner committed
236
237
238
239
240
        $join->type = 'LEFT';
        $join->extra = array();
        $join->extra_type = 'OR';
        foreach ($this->handler->value as $value) {
          $join->extra[] = array(
241
            'field' => $this->handler->realField,
dawehner's avatar
dawehner committed
242
243
244
245
246
            'value' => $value,
            'numeric' => !empty($this->handler->definition['numeric']),
          );
        }

247
        $this->handler->tableAlias = $this->addTable($join);
dawehner's avatar
dawehner committed
248
249
      }
    }
250
    return $this->handler->tableAlias;
dawehner's avatar
dawehner committed
251
252
253
254
255
  }

  /**
   * Provides a unique placeholders for handlers.
   */
256
  protected function placeholder() {
dawehner's avatar
dawehner committed
257
258
259
    return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
  }

260
  public function addFilter() {
dawehner's avatar
dawehner committed
261
262
263
    if (empty($this->handler->value)) {
      return;
    }
264
    $this->handler->ensureMyTable();
dawehner's avatar
dawehner committed
265
266

    // Shorten some variables:
267
    $field = $this->getField();
dawehner's avatar
dawehner committed
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
    $options = $this->handler->options;
    $operator = $this->handler->operator;
    $formula = !empty($this->formula);
    $value = $this->handler->value;
    if (empty($options['group'])) {
      $options['group'] = 0;
    }

    // add_condition determines whether a single expression is enough(FALSE) or the
    // conditions should be added via an db_or()/db_and() (TRUE).
    $add_condition = TRUE;
    if ($operator == 'not') {
      $value = NULL;
      $operator = 'IS NULL';
      $add_condition = FALSE;
    }
    elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
      if (count($value) > 1) {
        $operator = 'IN';
      }
      else {
        $value = is_array($value) ? array_pop($value) : $value;
        $operator = '=';
      }
      $add_condition = FALSE;
    }

    if (!$add_condition) {
      if ($formula) {
        $placeholder = $this->placeholder();
        if ($operator == 'IN') {
          $operator = "$operator IN($placeholder)";
        }
        else {
          $operator = "$operator $placeholder";
        }
        $placeholders = array(
          $placeholder => $value,
        ) + $this->placeholders;
307
        $this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
dawehner's avatar
dawehner committed
308
309
      }
      else {
310
311
        $placeholder = $this->placeholder();
        if (count($this->handler->value) > 1) {
312
          $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", array($placeholder => $value));
313
314
        }
        else {
315
          $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", array($placeholder => $value));
316
        }
dawehner's avatar
dawehner committed
317
318
319
320
      }
    }

    if ($add_condition) {
321
      $field = $this->handler->realField;
dawehner's avatar
dawehner committed
322
      $clause = $operator == 'or' ? db_or() : db_and();
323
      foreach ($this->handler->tableAliases as $value => $alias) {
dawehner's avatar
dawehner committed
324
325
326
327
        $clause->condition("$alias.$field", $value);
      }

      // implode on either AND or OR.
328
      $this->handler->query->addWhere($options['group'], $clause);
dawehner's avatar
dawehner committed
329
330
331
332
    }
  }

}