ManyToOneHelper.php 12.3 KB
Newer Older
1 2
<?php

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

8 9
namespace Drupal\views;

10
use Drupal\Core\Form\FormStateInterface;
11 12
use Drupal\views\Plugin\views\HandlerBase;

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
/**
 * 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;
  }

33
  public static function defineOptions(&$options) {
34
    $options['reduce_duplicates'] = array('default' => FALSE);
35 36
  }

37
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
38 39 40
    $form['reduce_duplicates'] = array(
      '#type' => 'checkbox',
      '#title' => t('Reduce duplicates'),
41
      '#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."),
42 43 44 45 46 47 48 49
      '#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
50
   * and implement handler->getFormula();
51
   */
52
  public function getField() {
53
    if (!empty($this->formula)) {
54
      return $this->handler->getFormula();
55 56
    }
    else {
57
      return $this->handler->tableAlias . '.' . $this->handler->realField;
58 59 60 61 62 63 64 65 66 67 68
    }
  }

  /**
   * 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.
   */
69
  public function addTable($join = NULL, $alias = NULL) {
70 71 72 73
    // This is used for lookups in the many_to_one table.
    $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;

    if (empty($join)) {
74
      $join = $this->getJoin();
75 76 77 78 79 80 81 82
    }

    // 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])) {
83
      $base_table = $this->handler->view->storage->get('base_table');
84 85 86 87 88 89
    }
    else {
      $base_table = $this->handler->query->relationships[$relationship]['base'];
    }

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

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

    // 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;
  }

115 116
  public function getJoin() {
    return $this->handler->getJoin();
117 118 119 120 121 122
  }

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

    // shortcuts
    $options = $this->handler->options;
129 130
    $view = $this->handler->view;
    $query = $this->handler->query;
131 132 133 134 135 136

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

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

  /**
157
   * Override ensureMyTable so we can control how this joins in.
158 159
   * The operator actually has influence over joining.
   */
160
  public function ensureMyTable() {
161
    if (!isset($this->handler->tableAlias)) {
162 163 164 165 166 167 168
      // 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.
169
          $join = $this->getJoin();
170 171 172
          if (isset($join)) {
            $join->type = 'INNER';
          }
173
          $this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
174 175 176
          $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
        }
        else {
177
          $join = $this->getJoin();
178 179 180 181 182
          $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(
183
                  'field' => $this->handler->realField,
184 185 186 187 188 189 190 191
                  'operator' => '!=',
                  'value' => $value,
                  'numeric' => !empty($this->handler->definition['numeric']),
                ),
              );
            }
          }

192
          $this->handler->tableAlias = $this->addTable($join);
193 194
        }

195
        return $this->handler->tableAlias;
196 197 198 199 200 201
      }

      // 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:
202
        $this->handler->tableAliases = array();
203
        foreach ($this->handler->value as $value) {
204
          $join = $this->getJoin();
205 206 207 208 209
          if ($this->handler->operator == 'and') {
            $join->type = 'INNER';
          }
          $join->extra = array(
            array(
210
              'field' => $this->handler->realField,
211 212 213 214 215 216 217 218 219 220 221 222 223 224
              '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]++);
          }

225 226
          $this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
          // Set tableAlias to the first of these.
227
          if (empty($this->handler->tableAlias)) {
228
            $this->handler->tableAlias = $this->handler->tableAliases[$value];
229 230 231 232 233 234 235
          }
        }
      }
      // 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 {
236
        $join = $this->getJoin();
237 238
        $join->type = 'LEFT';
        $join->extra = array();
239
        $join->extraOperator = 'OR';
240 241
        foreach ($this->handler->value as $value) {
          $join->extra[] = array(
242
            'field' => $this->handler->realField,
243 244 245 246 247
            'value' => $value,
            'numeric' => !empty($this->handler->definition['numeric']),
          );
        }

248
        $this->handler->tableAlias = $this->addTable($join);
249 250
      }
    }
251
    return $this->handler->tableAlias;
252 253 254 255 256
  }

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

261
  public function addFilter() {
262 263 264
    if (empty($this->handler->value)) {
      return;
    }
265
    $this->handler->ensureMyTable();
266 267

    // Shorten some variables:
268
    $field = $this->getField();
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 307
    $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;
308
        $this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
309 310
      }
      else {
311 312
        $placeholder = $this->placeholder();
        if (count($this->handler->value) > 1) {
313
          $placeholder .= '[]';
314 315 316 317 318 319 320

          if ($operator == 'IS NULL') {
            $this->handler->query->addWhereExpression(0, "$field $operator");
          }
          else {
            $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", array($placeholder => $value));
          }
321 322
        }
        else {
323 324 325 326 327 328
          if ($operator == 'IS NULL') {
            $this->handler->query->addWhereExpression(0, "$field $operator");
          }
          else {
            $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", array($placeholder => $value));
          }
329
        }
330 331 332 333
      }
    }

    if ($add_condition) {
334
      $field = $this->handler->realField;
335
      $clause = $operator == 'or' ? db_or() : db_and();
336
      foreach ($this->handler->tableAliases as $value => $alias) {
337 338 339 340
        $clause->condition("$alias.$field", $value);
      }

      // implode on either AND or OR.
341
      $this->handler->query->addWhere($options['group'], $clause);
342 343 344 345
    }
  }

}