3 namespace Drupal\views;
5 use Drupal\Core\Form\FormStateInterface;
6 use Drupal\views\Plugin\views\HandlerBase;
9 * This many to one helper object is used on both arguments and filters.
11 * @todo This requires extensive documentation on how this class is to
12 * be used. For now, look at the arguments and filters that use it. Lots
13 * of stuff is just pass-through but there are definitely some interesting
14 * areas where they interact.
16 * Any handler that uses this can have the following possibly additional
18 * - numeric: If true, treat this field as numeric, using %d instead of %s in
21 class ManyToOneHelper {
23 public function __construct($handler) {
24 $this->handler = $handler;
27 public static function defineOptions(&$options) {
28 $options['reduce_duplicates'] = ['default' => FALSE];
31 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
32 $form['reduce_duplicates'] = [
33 '#type' => 'checkbox',
34 '#title' => t('Reduce duplicates'),
35 '#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."),
36 '#default_value' => !empty($this->handler->options['reduce_duplicates']),
42 * Sometimes the handler might want us to use some kind of formula, so give
43 * it that option. If it wants us to do this, it must set $helper->formula = TRUE
44 * and implement handler->getFormula();
46 public function getField() {
47 if (!empty($this->formula)) {
48 return $this->handler->getFormula();
51 return $this->handler->tableAlias . '.' . $this->handler->realField;
56 * Add a table to the query.
58 * This is an advanced concept; not only does it add a new instance of the table,
59 * but it follows the relationship path all the way down to the relationship
60 * link point and adds *that* as a new relationship and then adds the table to
61 * the relationship, if necessary.
63 public function addTable($join = NULL, $alias = NULL) {
64 // This is used for lookups in the many_to_one table.
65 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
68 $join = $this->getJoin();
71 // See if there's a chain between us and the base relationship. If so, we need
72 // to create a new relationship to use.
73 $relationship = $this->handler->relationship;
75 // Determine the primary table to seek
76 if (empty($this->handler->query->relationships[$relationship])) {
77 $base_table = $this->handler->view->storage->get('base_table');
80 $base_table = $this->handler->query->relationships[$relationship]['base'];
83 // Cycle through the joins. This isn't as error-safe as the normal
84 // ensurePath logic. Perhaps it should be.
85 $r_join = clone $join;
86 while ($r_join->leftTable != $base_table) {
87 $r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
89 // If we found that there are tables in between, add the relationship.
90 if ($r_join->table != $join->table) {
91 $relationship = $this->handler->query->addRelationship($this->handler->table . '_' . $r_join->table, $r_join, $r_join->table, $this->handler->relationship);
94 // And now add our table, using the new relationship if one was used.
95 $alias = $this->handler->query->addTable($this->handler->table, $relationship, $join, $alias);
97 // Store what values are used by this table chain so that other chains can
98 // automatically discard those values.
99 if (empty($this->handler->view->many_to_one_tables[$field])) {
100 $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
103 $this->handler->view->many_to_one_tables[$field] = array_merge($this->handler->view->many_to_one_tables[$field], $this->handler->value);
109 public function getJoin() {
110 return $this->handler->getJoin();
114 * Provide the proper join for summary queries. This is important in part because
115 * it will cooperate with other arguments if possible.
117 public function summaryJoin() {
118 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
119 $join = $this->getJoin();
122 $options = $this->handler->options;
123 $view = $this->handler->view;
124 $query = $this->handler->query;
126 if (!empty($options['require_value'])) {
127 $join->type = 'INNER';
130 if (empty($options['add_table']) || empty($view->many_to_one_tables[$field])) {
131 return $query->ensureTable($this->handler->table, $this->handler->relationship, $join);
134 if (!empty($view->many_to_one_tables[$field])) {
135 foreach ($view->many_to_one_tables[$field] as $value) {
138 'field' => $this->handler->realField,
141 'numeric' => !empty($this->definition['numeric']),
146 return $this->addTable($join);
151 * Override ensureMyTable so we can control how this joins in.
152 * The operator actually has influence over joining.
154 public function ensureMyTable() {
155 if (!isset($this->handler->tableAlias)) {
156 // Case 1: Operator is an 'or' and we're not reducing duplicates.
157 // We hence get the absolute simplest:
158 $field = $this->handler->relationship . '_' . $this->handler->table . '.' . $this->handler->field;
159 if ($this->handler->operator == 'or' && empty($this->handler->options['reduce_duplicates'])) {
160 if (empty($this->handler->options['add_table']) && empty($this->handler->view->many_to_one_tables[$field])) {
161 // query optimization, INNER joins are slightly faster, so use them
162 // when we know we can.
163 $join = $this->getJoin();
165 $join->type = 'INNER';
167 $this->handler->tableAlias = $this->handler->query->ensureTable($this->handler->table, $this->handler->relationship, $join);
168 $this->handler->view->many_to_one_tables[$field] = $this->handler->value;
171 $join = $this->getJoin();
172 $join->type = 'LEFT';
173 if (!empty($this->handler->view->many_to_one_tables[$field])) {
174 foreach ($this->handler->view->many_to_one_tables[$field] as $value) {
177 'field' => $this->handler->realField,
180 'numeric' => !empty($this->handler->definition['numeric']),
186 $this->handler->tableAlias = $this->addTable($join);
189 return $this->handler->tableAlias;
192 // Case 2: it's an 'and' or an 'or'.
193 // We do one join per selected value.
194 if ($this->handler->operator != 'not') {
195 // Clone the join for each table:
196 $this->handler->tableAliases = [];
197 foreach ($this->handler->value as $value) {
198 $join = $this->getJoin();
199 if ($this->handler->operator == 'and') {
200 $join->type = 'INNER';
204 'field' => $this->handler->realField,
206 'numeric' => !empty($this->handler->definition['numeric']),
210 // The table alias needs to be unique to this value across the
211 // multiple times the filter or argument is called by the view.
212 if (!isset($this->handler->view->many_to_one_aliases[$field][$value])) {
213 if (!isset($this->handler->view->many_to_one_count[$this->handler->table])) {
214 $this->handler->view->many_to_one_count[$this->handler->table] = 0;
216 $this->handler->view->many_to_one_aliases[$field][$value] = $this->handler->table . '_value_' . ($this->handler->view->many_to_one_count[$this->handler->table]++);
219 $this->handler->tableAliases[$value] = $this->addTable($join, $this->handler->view->many_to_one_aliases[$field][$value]);
220 // Set tableAlias to the first of these.
221 if (empty($this->handler->tableAlias)) {
222 $this->handler->tableAlias = $this->handler->tableAliases[$value];
226 // Case 3: it's a 'not'.
227 // We just do one join. We'll add a where clause during
228 // the query phase to ensure that $table.$field IS NULL.
230 $join = $this->getJoin();
231 $join->type = 'LEFT';
233 $join->extraOperator = 'OR';
234 foreach ($this->handler->value as $value) {
236 'field' => $this->handler->realField,
238 'numeric' => !empty($this->handler->definition['numeric']),
242 $this->handler->tableAlias = $this->addTable($join);
245 return $this->handler->tableAlias;
249 * Provides a unique placeholders for handlers.
251 protected function placeholder() {
252 return $this->handler->query->placeholder($this->handler->options['table'] . '_' . $this->handler->options['field']);
255 public function addFilter() {
256 if (empty($this->handler->value)) {
259 $this->handler->ensureMyTable();
261 // Shorten some variables:
262 $field = $this->getField();
263 $options = $this->handler->options;
264 $operator = $this->handler->operator;
265 $formula = !empty($this->formula);
266 $value = $this->handler->value;
267 if (empty($options['group'])) {
268 $options['group'] = 0;
271 // add_condition determines whether a single expression is enough(FALSE) or the
272 // conditions should be added via an db_or()/db_and() (TRUE).
273 $add_condition = TRUE;
274 if ($operator == 'not') {
276 $operator = 'IS NULL';
277 $add_condition = FALSE;
279 elseif ($operator == 'or' && empty($options['reduce_duplicates'])) {
280 if (count($value) > 1) {
284 $value = is_array($value) ? array_pop($value) : $value;
287 $add_condition = FALSE;
290 if (!$add_condition) {
292 $placeholder = $this->placeholder();
293 if ($operator == 'IN') {
294 $operator = "$operator IN($placeholder)";
297 $operator = "$operator $placeholder";
300 $placeholder => $value,
302 $this->handler->query->addWhereExpression($options['group'], "$field $operator", $placeholders);
305 $placeholder = $this->placeholder();
306 if (count($this->handler->value) > 1) {
307 $placeholder .= '[]';
309 if ($operator == 'IS NULL') {
310 $this->handler->query->addWhereExpression(0, "$field $operator");
313 $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", [$placeholder => $value]);
317 if ($operator == 'IS NULL') {
318 $this->handler->query->addWhereExpression(0, "$field $operator");
321 $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", [$placeholder => $value]);
327 if ($add_condition) {
328 $field = $this->handler->realField;
329 $clause = $operator == 'or' ? db_or() : db_and();
330 foreach ($this->handler->tableAliases as $value => $alias) {
331 $clause->condition("$alias.$field", $value);
334 // implode on either AND or OR.
335 $this->handler->query->addWhere($options['group'], $clause);