annotate core/modules/views/src/ManyToOneHelper.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\views;
Chris@0 4
Chris@0 5 use Drupal\Core\Database\Query\Condition;
Chris@0 6 use Drupal\Core\Form\FormStateInterface;
Chris@0 7 use Drupal\views\Plugin\views\HandlerBase;
Chris@0 8
Chris@0 9 /**
Chris@0 10 * This many to one helper object is used on both arguments and filters.
Chris@0 11 *
Chris@0 12 * @todo This requires extensive documentation on how this class is to
Chris@0 13 * be used. For now, look at the arguments and filters that use it. Lots
Chris@0 14 * of stuff is just pass-through but there are definitely some interesting
Chris@0 15 * areas where they interact.
Chris@0 16 *
Chris@0 17 * Any handler that uses this can have the following possibly additional
Chris@0 18 * definition terms:
Chris@0 19 * - numeric: If true, treat this field as numeric, using %d instead of %s in
Chris@0 20 * queries.
Chris@0 21 */
Chris@0 22 class ManyToOneHelper {
Chris@0 23
Chris@0 24 public function __construct($handler) {
Chris@0 25 $this->handler = $handler;
Chris@0 26 }
Chris@0 27
Chris@0 28 public static function defineOptions(&$options) {
Chris@0 29 $options['reduce_duplicates'] = ['default' => FALSE];
Chris@0 30 }
Chris@0 31
Chris@0 32 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
Chris@0 33 $form['reduce_duplicates'] = [
Chris@0 34 '#type' => 'checkbox',
Chris@0 35 '#title' => t('Reduce duplicates'),
Chris@0 36 '#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."),
Chris@0 37 '#default_value' => !empty($this->handler->options['reduce_duplicates']),
Chris@0 38 '#weight' => 4,
Chris@0 39 ];
Chris@0 40 }
Chris@0 41
Chris@0 42 /**
Chris@0 43 * Sometimes the handler might want us to use some kind of formula, so give
Chris@0 44 * it that option. If it wants us to do this, it must set $helper->formula = TRUE
Chris@0 45 * and implement handler->getFormula();
Chris@0 46 */
Chris@0 47 public function getField() {
Chris@0 48 if (!empty($this->formula)) {
Chris@0 49 return $this->handler->getFormula();
Chris@0 50 }
Chris@0 51 else {
Chris@0 52 return $this->handler->tableAlias . '.' . $this->handler->realField;
Chris@0 53 }
Chris@0 54 }
Chris@0 55
Chris@0 56 /**
Chris@0 57 * Add a table to the query.
Chris@0 58 *
Chris@0 59 * This is an advanced concept; not only does it add a new instance of the table,
Chris@0 60 * but it follows the relationship path all the way down to the relationship
Chris@0 61 * link point and adds *that* as a new relationship and then adds the table to
Chris@0 62 * the relationship, if necessary.
Chris@0 63 */
Chris@0 64 public function addTable($join = NULL, $alias = NULL) {
Chris@0 65 // This is used for lookups in the many_to_one table.
Chris@0 66 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
Chris@0 67
Chris@0 68 if (empty($join)) {
Chris@0 69 $join = $this->getJoin();
Chris@0 70 }
Chris@0 71
Chris@0 72 // See if there's a chain between us and the base relationship. If so, we need
Chris@0 73 // to create a new relationship to use.
Chris@0 74 $relationship = $this->handler->relationship;
Chris@0 75
Chris@0 76 // Determine the primary table to seek
Chris@0 77 if (empty($this->handler->query->relationships[$relationship])) {
Chris@0 78 $base_table = $this->handler->view->storage->get('base_table');
Chris@0 79 }
Chris@0 80 else {
Chris@0 81 $base_table = $this->handler->query->relationships[$relationship]['base'];
Chris@0 82 }
Chris@0 83
Chris@0 84 // Cycle through the joins. This isn't as error-safe as the normal
Chris@0 85 // ensurePath logic. Perhaps it should be.
Chris@0 86 $r_join = clone $join;
Chris@0 87 while ($r_join->leftTable != $base_table) {
Chris@0 88 $r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
Chris@0 89 }
Chris@0 90 // If we found that there are tables in between, add the relationship.
Chris@0 91 if ($r_join->table != $join->table) {
Chris@0 92 $relationship = $this->handler->query->addRelationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
Chris@0 93 }
Chris@0 94
Chris@0 95 // And now add our table, using the new relationship if one was used.
Chris@0 96 $alias = $this->handler->query->addTable($this->handler->table, $relationship, $join, $alias);
Chris@0 97
Chris@0 98 // Store what values are used by this table chain so that other chains can
Chris@0 99 // automatically discard those values.
Chris@0 100 if (empty($this->handler->view->many_to_one_tables[$field])) {
Chris@0 101 $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
Chris@0 102 }
Chris@0 103 else {
Chris@0 104 $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
Chris@0 105 }
Chris@0 106
Chris@0 107 return $alias;
Chris@0 108 }
Chris@0 109
Chris@0 110 public function getJoin() {
Chris@0 111 return $this->handler->getJoin();
Chris@0 112 }
Chris@0 113
Chris@0 114 /**
Chris@0 115 * Provide the proper join for summary queries. This is important in part because
Chris@0 116 * it will cooperate with other arguments if possible.
Chris@0 117 */
Chris@0 118 public function summaryJoin() {
Chris@0 119 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
Chris@0 120 $join = $this->getJoin();
Chris@0 121
Chris@0 122 // shortcuts
Chris@0 123 $options = $this->handler->options;
Chris@0 124 $view = $this->handler->view;
Chris@0 125 $query = $this->handler->query;
Chris@0 126
Chris@0 127 if (!empty($options['require_value'])) {
Chris@0 128 $join->type = 'INNER';
Chris@0 129 }
Chris@0 130
Chris@0 131 if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
Chris@0 132 return $query->ensureTable($this->handler->table, $this->handler->relationship, $join);
Chris@0 133 }
Chris@0 134 else {
Chris@0 135 if (!empty($view->many_to_one_tables[$field])) {
Chris@0 136 foreach ($view->many_to_one_tables[$field] as $value) {
Chris@0 137 $join->extra = [
Chris@0 138 [
Chris@0 139 'field' => $this->handler->realField,
Chris@0 140 'operator' => '!=',
Chris@0 141 'value' => $value,
Chris@0 142 'numeric' => !empty($this->definition['numeric']),
Chris@0 143 ],
Chris@0 144 ];
Chris@0 145 }
Chris@0 146 }
Chris@0 147 return $this->addTable($join);
Chris@0 148 }
Chris@0 149 }
Chris@0 150
Chris@0 151 /**
Chris@0 152 * Override ensureMyTable so we can control how this joins in.
Chris@0 153 * The operator actually has influence over joining.
Chris@0 154 */
Chris@0 155 public function ensureMyTable() {
Chris@0 156 if (!isset($this->handler->tableAlias)) {
Chris@0 157 // Case 1: Operator is an 'or' and we're not reducing duplicates.
Chris@0 158 // We hence get the absolute simplest:
Chris@0 159 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
Chris@0 160 if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
Chris@0 161 if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
Chris@0 162 // query optimization, INNER joins are slightly faster, so use them
Chris@0 163 // when we know we can.
Chris@0 164 $join = $this->getJoin();
Chris@0 165 if (isset($join)) {
Chris@0 166 $join->type = 'INNER';
Chris@0 167 }
Chris@0 168 $this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
Chris@0 169 $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
Chris@0 170 }
Chris@0 171 else {
Chris@0 172 $join = $this->getJoin();
Chris@0 173 $join->type = 'LEFT';
Chris@0 174 if (!empty($this->handler->view->many_to_one_tables[$field])) {
Chris@0 175 foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
Chris@0 176 $join->extra = [
Chris@0 177 [
Chris@0 178 'field' => $this->handler->realField,
Chris@0 179 'operator' => '!=',
Chris@0 180 'value' => $value,
Chris@0 181 'numeric' => !empty($this->handler->definition['numeric']),
Chris@0 182 ],
Chris@0 183 ];
Chris@0 184 }
Chris@0 185 }
Chris@0 186
Chris@0 187 $this->handler->tableAlias = $this->addTable($join);
Chris@0 188 }
Chris@0 189
Chris@0 190 return $this->handler->tableAlias;
Chris@0 191 }
Chris@0 192
Chris@0 193 // Case 2: it's an 'and' or an 'or'.
Chris@0 194 // We do one join per selected value.
Chris@0 195 if ($this->handler->operator != 'not') {
Chris@0 196 // Clone the join for each table:
Chris@0 197 $this->handler->tableAliases = [];
Chris@0 198 foreach ($this->handler->value as $value) {
Chris@0 199 $join = $this->getJoin();
Chris@0 200 if ($this->handler->operator == 'and') {
Chris@0 201 $join->type = 'INNER';
Chris@0 202 }
Chris@0 203 $join->extra = [
Chris@0 204 [
Chris@0 205 'field' => $this->handler->realField,
Chris@0 206 'value' => $value,
Chris@0 207 'numeric' => !empty($this->handler->definition['numeric']),
Chris@0 208 ],
Chris@0 209 ];
Chris@0 210
Chris@0 211 // The table alias needs to be unique to this value across the
Chris@0 212 // multiple times the filter or argument is called by the view.
Chris@0 213 if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
Chris@0 214 if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
Chris@0 215 $this->handler->view->many_to_one_count[$this->handler->table] = 0;
Chris@0 216 }
Chris@0 217 $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
Chris@0 218 }
Chris@0 219
Chris@0 220 $this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
Chris@0 221 // Set tableAlias to the first of these.
Chris@0 222 if (empty($this->handler->tableAlias)) {
Chris@0 223 $this->handler->tableAlias = $this->handler->tableAliases[$value];
Chris@0 224 }
Chris@0 225 }
Chris@0 226 }
Chris@0 227 // Case 3: it's a 'not'.
Chris@0 228 // We just do one join. We'll add a where clause during
Chris@0 229 // the query phase to ensure that $table.$field IS NULL.
Chris@0 230 else {
Chris@0 231 $join = $this->getJoin();
Chris@0 232 $join->type = 'LEFT';
Chris@0 233 $join->extra = [];
Chris@0 234 $join->extraOperator = 'OR';
Chris@0 235 foreach ($this->handler->value as $value) {
Chris@0 236 $join->extra[] = [
Chris@0 237 'field' => $this->handler->realField,
Chris@0 238 'value' => $value,
Chris@0 239 'numeric' => !empty($this->handler->definition['numeric']),
Chris@0 240 ];
Chris@0 241 }
Chris@0 242
Chris@0 243 $this->handler->tableAlias = $this->addTable($join);
Chris@0 244 }
Chris@0 245 }
Chris@0 246 return $this->handler->tableAlias;
Chris@0 247 }
Chris@0 248
Chris@0 249 /**
Chris@0 250 * Provides a unique placeholders for handlers.
Chris@0 251 */
Chris@0 252 protected function placeholder() {
Chris@0 253 return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
Chris@0 254 }
Chris@0 255
Chris@0 256 public function addFilter() {
Chris@0 257 if (empty($this->handler->value)) {
Chris@0 258 return;
Chris@0 259 }
Chris@0 260 $this->handler->ensureMyTable();
Chris@0 261
Chris@0 262 // Shorten some variables:
Chris@0 263 $field = $this->getField();
Chris@0 264 $options = $this->handler->options;
Chris@0 265 $operator = $this->handler->operator;
Chris@0 266 $formula = !empty($this->formula);
Chris@0 267 $value = $this->handler->value;
Chris@0 268 if (empty($options['group'])) {
Chris@0 269 $options['group'] = 0;
Chris@0 270 }
Chris@0 271
Chris@0 272 // If $add_condition is set to FALSE, a single expression is enough. If it
Chris@0 273 // is set to TRUE, conditions will be added.
Chris@0 274 $add_condition = TRUE;
Chris@0 275 if ($operator == 'not') {
Chris@0 276 $value = NULL;
Chris@0 277 $operator = 'IS NULL';
Chris@0 278 $add_condition = FALSE;
Chris@0 279 }
Chris@0 280 elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
Chris@0 281 if (count($value) > 1) {
Chris@0 282 $operator = 'IN';
Chris@0 283 }
Chris@0 284 else {
Chris@0 285 $value = is_array($value) ? array_pop($value) : $value;
Chris@0 286 $operator = '=';
Chris@0 287 }
Chris@0 288 $add_condition = FALSE;
Chris@0 289 }
Chris@0 290
Chris@0 291 if (!$add_condition) {
Chris@0 292 if ($formula) {
Chris@0 293 $placeholder = $this->placeholder();
Chris@0 294 if ($operator == 'IN') {
Chris@0 295 $operator = "$operator IN($placeholder)";
Chris@0 296 }
Chris@0 297 else {
Chris@0 298 $operator = "$operator $placeholder";
Chris@0 299 }
Chris@0 300 $placeholders = [
Chris@0 301 $placeholder => $value,
Chris@0 302 ];
Chris@0 303 $this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
Chris@0 304 }
Chris@0 305 else {
Chris@0 306 $placeholder = $this->placeholder();
Chris@0 307 if (count($this->handler->value) > 1) {
Chris@0 308 $placeholder .= '[]';
Chris@0 309
Chris@0 310 if ($operator == 'IS NULL') {
Chris@0 311 $this->handler->query->addWhereExpression(0, "$field $operator");
Chris@0 312 }
Chris@0 313 else {
Chris@0 314 $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", [$placeholder => $value]);
Chris@0 315 }
Chris@0 316 }
Chris@0 317 else {
Chris@0 318 if ($operator == 'IS NULL') {
Chris@0 319 $this->handler->query->addWhereExpression(0, "$field $operator");
Chris@0 320 }
Chris@0 321 else {
Chris@0 322 $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", [$placeholder => $value]);
Chris@0 323 }
Chris@0 324 }
Chris@0 325 }
Chris@0 326 }
Chris@0 327
Chris@0 328 if ($add_condition) {
Chris@0 329 $field = $this->handler->realField;
Chris@0 330 $clause = $operator == 'or' ? new Condition('OR') : new Condition('AND');
Chris@0 331 foreach ($this->handler->tableAliases as $value => $alias) {
Chris@0 332 $clause->condition("$alias.$field", $value);
Chris@0 333 }
Chris@0 334
Chris@0 335 // implode on either AND or OR.
Chris@0 336 $this->handler->query->addWhere($options['group'], $clause);
Chris@0 337 }
Chris@0 338 }
Chris@0 339
Chris@0 340 }