QueryGenerator::getColumnName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 5
cts 7
cp 0.7143
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2.0932
1
<?php
2
/**
3
 * Query generator file.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 * @author    Radosław Skrzypczak <[email protected]>
11
 */
12
13
namespace App;
14
15
/**
16
 * Query generator class.
17
 */
18
class QueryGenerator
19
{
20
	const STRING_TYPE = ['string', 'text', 'email', 'reference'];
21
	const NUMERIC_TYPE = ['integer', 'double', 'currency', 'currencyInventory'];
22
	const DATE_TYPE = ['date', 'datetime'];
23
	const EQUALITY_TYPES = ['currency', 'percentage', 'double', 'integer', 'number'];
24
	const COMMA_TYPES = ['picklist', 'multipicklist', 'owner', 'date', 'datetime', 'time', 'tree', 'sharedOwner', 'sharedOwner'];
25
26
	/**
27
	 * State records to display
28
	 * 0 - Active
29
	 * 1 - Trash
30
	 * 2 - Archived.
31
	 *
32
	 * @var int|null
33
	 */
34
	private $stateCondition = 0;
35
36
	/** @var bool Permissions conditions */
37
	public $permissions = true;
38
39
	/** @var string Module name */
40
	private $moduleName;
41
42
	/** @var \App\Db\Query */
43
	private $query;
44
45
	/** @var \App\Db\Query */
46
	private $buildedQuery;
47
	private $fields = [];
48
	private $referenceFields = [];
49
	private $ownerFields = [];
50
	private $customColumns = [];
51
	private $advFilterList;
52
	private $conditions;
53
54
	/** @var array Advanced conditions */
55
	private $advancedConditions = [];
56
57
	/** @var array Search fields for duplicates. */
58
	private $searchFieldsForDuplicates = [];
59
60
	/** @var array Joins */
61
	private $joins = [];
62
63
	/** @var string[] Tables list */
64
	private $tablesList = [];
65
	private $queryFields = [];
66
	private $order = [];
67
	private $group = [];
68
	private $sourceRecord;
69
	private $concatColumn = [];
70
	private $relatedFields = [];
71
	private $relatedQueryFields = [];
72
73
	/**
74
	 * @var bool
75
	 */
76
	private $ignoreComma = false;
77
78
	/**
79
	 * @var array Required conditions
80
	 */
81
	private $conditionsAnd = [];
82
83
	/**
84
	 * @var array Optional conditions
85
	 */
86
	private $conditionsOr = [];
87
88
	/**
89
	 * @var \Vtiger_Module_Model
90
	 */
91
	private $moduleModel;
92
93
	/**
94
	 * @var Vtiger_Field_Model[]
95
	 */
96
	private $fieldsModel;
97
98
	/**
99
	 * @var Vtiger_Field_Model[]
100
	 */
101
	private $relatedFieldsModel;
102
103
	/**
104
	 * @var \CRMEntity
105
	 */
106
	private $entityModel;
107
108
	/** @var User */
109
	private $user;
110
111
	/** @var int|null Limit */
112
	private $limit;
113
114
	/** @var int|null Offset */
115
	private $offset;
116
117
	/** @var string|null Distinct field */
118
	private $distinct;
119
120
	/**
121
	 * QueryGenerator construct.
122
	 *
123 4
	 * @param string $moduleName
124
	 * @param mixed  $userId
125 4
	 */
126 4
	public function __construct($moduleName, $userId = false)
127 4
	{
128 4
		$this->moduleName = $moduleName;
129 4
		$this->moduleModel = \Vtiger_Module_Model::getInstance($moduleName);
130
		$this->entityModel = \CRMEntity::getInstance($moduleName);
131
		$this->user = User::getUserModel($userId ?: User::getCurrentUserId());
132
	}
133
134
	/**
135
	 * Get module name.
136 1
	 *
137
	 * @return string
138 1
	 */
139
	public function getModule()
140
	{
141
		return $this->moduleName;
142
	}
143
144
	/**
145
	 * Get module model.
146
	 *
147
	 * @return \Vtiger_Module_Model
148
	 */
149
	public function getModuleModel()
150
	{
151
		return $this->moduleModel;
152
	}
153
154
	/**
155
	 * Get query fields.
156 1
	 *
157
	 * @return string[]
158 1
	 */
159
	public function getFields()
160
	{
161
		return $this->fields;
162
	}
163
164
	/**
165
	 * Get list view query fields.
166
	 *
167
	 * @return \Vtiger_Field_Model[]
168
	 */
169
	public function getListViewFields(): array
170
	{
171
		$headerFields = [];
172
		foreach ($this->getFields() as $fieldName) {
173
			if ($model = $this->getModuleField($fieldName)) {
174
				$headerFields[$fieldName] = $model;
175
				if ($field = $this->getQueryField($fieldName)->getListViewFields()) {
176
					$headerFields[$field->getName()] = $field;
177
					$this->fields[] = $field->getName();
178
				}
179
			}
180
		}
181
		return $headerFields;
182
	}
183
184
	/**
185
	 * Sets conditions from ConditionBuilder.
186 2
	 *
187
	 * @param array $conditions
188 2
	 *
189 2
	 * @return $this
190
	 */
191
	public function setConditions(array $conditions)
192
	{
193
		$this->conditions = $conditions;
194
195
		return $this;
196
	}
197
198 3
	/**
199
	 * Set query fields.
200 3
	 *
201 3
	 * @param string[] $fields
202
	 *
203
	 * @return \self
0 ignored issues
show
Bug introduced by
The type self was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
204
	 */
205
	public function setFields(array $fields)
206
	{
207
		$this->fields = [];
208
		foreach ($fields as $fieldName) {
209
			$this->setField($fieldName);
210
		}
211
212
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
213
	}
214
215
	/**
216
	 * Set query offset.
217
	 *
218
	 * @param int $offset
219
	 *
220
	 * @return \self
221
	 */
222
	public function setOffset($offset)
223
	{
224
		$this->offset = $offset;
225
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
226
	}
227
228
	/**
229
	 * Set query limit.
230
	 *
231
	 * @param int $limit
232
	 *
233
	 * @return \self
234
	 */
235
	public function setLimit($limit)
236
	{
237
		$this->limit = $limit;
238
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
239
	}
240
241
	/**
242
	 * Get query limit.
243
	 */
244
	public function getLimit()
245
	{
246
		return $this->limit;
247
	}
248
249
	/**
250
	 * Set distinct column.
251
	 *
252
	 * @param string $columnName
253
	 *
254
	 * @return \self
255
	 */
256 1
	public function setDistinct($columnName)
257
	{
258 1
		$this->distinct = $columnName;
259
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
260
	}
261
262
	/**
263
	 * Get distinct column.
264
	 *
265
	 * @return string|null
266
	 */
267
	public function getDistinct(): ?string
268 3
	{
269
		return $this->distinct;
270 3
	}
271
272
	/**
273
	 * Returns related fields.
274
	 *
275 3
	 * @return array
276
	 */
277 3
	public function getRelatedFields()
278
	{
279
		return $this->relatedFields;
280
	}
281
282
	/**
283
	 * Set query field.
284
	 *
285
	 * @param string $fieldName
286
	 *
287
	 * @return \self
288
	 */
289
	public function setField(string $fieldName): self
290
	{
291
		if (false !== strpos($fieldName, ':')) {
292
			[$relatedFieldName, $relatedModule, $sourceField] = array_pad(explode(':', $fieldName), 3, null);
293
			$this->addRelatedField([
294
				'sourceField' => $sourceField,
295
				'relatedModule' => $relatedModule,
296
				'relatedField' => $relatedFieldName
297 1
			]);
298
		} else {
299 1
			$this->fields[] = $fieldName;
300 1
		}
301 1
302 1
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
303
	}
304
305
	/**
306
	 * Clear fields.
307
	 *
308
	 * @return self
309
	 */
310 1
	public function clearFields(): self
311
	{
312
		$this->fields = ['id'];
313
		$this->relatedFields = [];
314
		$this->customColumns = [];
315
		return $this;
316
	}
317
318
	/**
319
	 * Load base module list fields.
320
	 */
321
	public function loadListFields()
322
	{
323
		$listFields = $this->entityModel->list_fields_name;
324
		$listFields[] = 'id';
325
		$this->fields = $listFields;
326
	}
327
328
	/**
329
	 * Set custom column.
330
	 *
331
	 * @param string|string[] $columns
332
	 *
333
	 * @return \self
334 1
	 */
335
	public function setCustomColumn($columns): self
336 1
	{
337
		if (\is_array($columns)) {
338
			foreach ($columns as $key => $column) {
339
				if (is_numeric($key)) {
340
					$this->customColumns[] = $column;
341
				} else {
342
					$this->customColumns[$key] = $column;
343
				}
344
			}
345
		} else {
346
			$this->customColumns[] = $columns;
347
		}
348
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
349
	}
350
351
	/**
352
	 * Set concat column.
353
	 *
354
	 * @param string $fieldName
355
	 * @param string $concat
356
	 *
357 2
	 * @return \self
358
	 */
359 2
	public function setConcatColumn(string $fieldName, string $concat)
360 2
	{
361
		$this->concatColumn[$fieldName] = $concat;
362
363
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
364 2
	}
365
366
	/**
367
	 * Get CRMEntity Model.
368
	 *
369
	 * @return \CRMEntity
370
	 */
371
	public function getEntityModel()
372 4
	{
373
		return $this->entityModel;
374 4
	}
375 4
376 4
	/**
377
	 * Get reference fields.
378
	 *
379
	 * @param string $fieldName
380
	 *
381
	 * @return array
382
	 */
383
	public function getReference($fieldName)
384
	{
385
		return $this->referenceFields[$fieldName];
386
	}
387
388
	/**
389
	 * Add a mandatory condition.
390 4
	 *
391
	 * @param array $condition
392
	 * @param bool  $groupAnd
393
	 */
394
	public function addNativeCondition($condition, $groupAnd = true)
395
	{
396
		if ($groupAnd) {
397
			$this->conditionsAnd[] = $condition;
398
		} else {
399
			$this->conditionsOr[] = $condition;
400
		}
401
		return $this;
402
	}
403
404
	/**
405
	 * Returns related fields for section SELECT.
406
	 *
407
	 * @return array
408
	 */
409
	public function loadRelatedFields()
410
	{
411
		$fields = $checkIds = [];
412 2
		foreach ($this->relatedFields as $field) {
413
			$joinTableName = $this->getModuleField($field['sourceField'])->getTableName();
414 2
			$moduleTableIndexList = $this->entityModel->tab_name_index;
415 2
			$baseTable = $this->entityModel->table_name;
416
			if ($joinTableName !== $baseTable) {
417
				$this->addJoin(['INNER JOIN', $joinTableName, "{$baseTable}.{$moduleTableIndexList[$baseTable]} = {$joinTableName}.{$moduleTableIndexList[$joinTableName]}"]);
418
			}
419
			$relatedFieldModel = $this->addRelatedJoin($field);
420
			$fields["{$field['sourceField']}{$field['relatedModule']}{$relatedFieldModel->getName()}"] = "{$relatedFieldModel->getTableName()}{$field['sourceField']}.{$relatedFieldModel->getColumnName()}";
421
			if (!isset($checkIds[$field['sourceField']][$field['relatedModule']])) {
422 1
				$checkIds[$field['sourceField']][$field['relatedModule']] = $field['relatedModule'];
423
				$fields["{$field['sourceField']}{$field['relatedModule']}id"] = $relatedFieldModel->getTableName() . $field['sourceField'] . '.' . \Vtiger_CRMEntity::getInstance($field['relatedModule'])->tab_name_index[$relatedFieldModel->getTableName()];
424 1
			}
425 1
		}
426
		return $fields;
427 1
	}
428
429
	/**
430
	 * Set related field.
431
	 *
432
	 * @param string[] $field
433
	 *
434
	 * @return \self
435 2
	 */
436
	public function addRelatedField($field)
437 2
	{
438 2
		if (!\in_array($field, $this->relatedFields)) {
439
			$this->relatedFields[] = $field;
440
		}
441
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
442
	}
443
444
	/**
445
	 * Set source record.
446
	 *
447
	 * @param int $sourceRecord
448
	 *
449
	 * @return $this
450
	 */
451
	public function setSourceRecord(int $sourceRecord)
452
	{
453
		$this->sourceRecord = $sourceRecord;
454
		return $this;
455
	}
456
457
	/**
458
	 * Appends a JOIN part to the query.
459
	 *
460
	 * @example ['INNER JOIN', 'vtiger_user2role', 'vtiger_user2role.userid = vtiger_users.id']
461
	 *
462
	 * @param array $join
463
	 *
464
	 * @return $this
465
	 */
466
	public function addJoin($join)
467
	{
468
		if (!isset($this->joins[$join[1]])) {
469
			$this->joins[$join[1]] = $join;
470
		}
471
		return $this;
472
	}
473
474
	/**
475
	 * Add table to query.
476
	 *
477
	 * @param string $tableName
478
	 */
479
	public function addTableToQuery($tableName)
480
	{
481
		$this->tablesList[$tableName] = $tableName;
482
		return $this;
483
	}
484
485
	/**
486
	 * Set ignore comma.
487
	 *
488
	 * @param bool $val
489
	 */
490
	public function setIgnoreComma($val)
491
	{
492
		$this->ignoreComma = $val;
493
	}
494
495
	/**
496
	 * Get ignore comma.
497
	 *
498
	 * @return bool
499
	 */
500
	public function getIgnoreComma()
501
	{
502
		return $this->ignoreComma;
503
	}
504
505
	/**
506
	 * Set order.
507
	 *
508
	 * @param string $fieldName
509
	 * @param string $order     ASC/DESC
510
	 *
511
	 * @return \self
512
	 */
513
	public function setOrder($fieldName, $order = false)
514
	{
515
		$queryField = $this->getQueryField($fieldName);
516
		$this->order = array_merge($this->order, $queryField->getOrderBy($order));
517
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
518
	}
519
520
	/**
521
	 * Set group.
522
	 *
523
	 * @param string $fieldName
524
	 *
525
	 * @return \self
526
	 */
527
	public function setGroup($fieldName)
528
	{
529
		$queryField = $this->getQueryField($fieldName);
530
		$this->group[] = $queryField->getColumnName();
531
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
532
	}
533
534 4
	/**
535
	 * Set custom group.
536 4
	 *
537 2
	 * @param array|string $groups
538
	 *
539 4
	 * @return \self
540 4
	 */
541 4
	public function setCustomGroup($groups)
542 3
	{
543
		if (\is_array($groups)) {
544 4
			foreach ($groups as $key => $group) {
545 4
				if (is_numeric($key)) {
546
					$this->group[] = $group;
547
				} else {
548 4
					$this->group[$key] = $group;
549
				}
550
			}
551
		} else {
552
			$this->group[] = $groups;
553
		}
554
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type App\QueryGenerator which is incompatible with the documented return type self.
Loading history...
555
	}
556
557
	/**
558
	 * Function sets the field for which the duplicated values will be searched.
559
	 *
560
	 * @param string   $fieldName
561
	 * @param bool|int $ignoreEmptyValue
562
	 */
563
	public function setSearchFieldsForDuplicates($fieldName, $ignoreEmptyValue = true)
564
	{
565
		$field = $this->getModuleField($fieldName);
566
		if ($field && !isset($this->tablesList[$field->getTableName()])) {
567
			$this->tablesList[$field->getTableName()] = $field->getTableName();
568
		}
569
		$this->searchFieldsForDuplicates[$fieldName] = $ignoreEmptyValue;
570
	}
571
572
	/**
573 4
	 * Get fields module.
574
	 *
575 4
	 * @return array
576 2
	 */
577
	public function getModuleFields()
578 4
	{
579 4
		if ($this->fieldsModel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fieldsModel of type App\Vtiger_Field_Model[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
580
			return $this->fieldsModel;
581
		}
582
		$moduleFields = $this->moduleModel->getFields();
583
		foreach ($moduleFields as $fieldName => &$fieldModel) {
584
			if ($fieldModel->isReferenceField()) {
585
				$this->referenceFields[$fieldName] = $fieldModel->getReferenceList();
586
			}
587
			if ('owner' === $fieldModel->getFieldDataType()) {
588
				$this->ownerFields[] = $fieldName;
589
			}
590
		}
591
		return $this->fieldsModel = $moduleFields;
0 ignored issues
show
Documentation Bug introduced by
It seems like $moduleFields of type Vtiger_Field_Model[] is incompatible with the declared type App\Vtiger_Field_Model[] of property $fieldsModel.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
592
	}
593
594
	/**
595
	 * Get fields module.
596
	 *
597
	 * @param string $moduleName
598
	 *
599
	 * @return \Vtiger_Field_Model[]
600
	 */
601
	public function getRelatedModuleFields(string $moduleName)
602
	{
603
		if (isset($this->relatedFieldsModel[$moduleName])) {
604
			return $this->relatedFieldsModel[$moduleName];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->relatedFieldsModel[$moduleName] returns the type App\Vtiger_Field_Model which is incompatible with the documented return type Vtiger_Field_Model[].
Loading history...
605
		}
606
		return $this->relatedFieldsModel[$moduleName] = \Vtiger_Module_Model::getInstance($moduleName)->getFields();
607
	}
608
609
	/**
610
	 * Get field module.
611
	 *
612
	 * @param string $fieldName
613
	 *
614
	 * @return \Vtiger_Field_Model|bool
615
	 */
616
	public function getModuleField(string $fieldName)
617
	{
618
		if (!$this->fieldsModel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fieldsModel of type App\Vtiger_Field_Model[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
619
			$this->getModuleFields();
620
		}
621
		if (isset($this->fieldsModel[$fieldName])) {
622
			return $this->fieldsModel[$fieldName];
623
		}
624
		return false;
625
	}
626 2
627
	/**
628 2
	 *  Get field in related module.
629 2
	 *
630 2
	 * @param string $fieldName
631
	 * @param string $moduleName
632
	 *
633 2
	 * @return \Vtiger_Field_Model|bool
634 2
	 */
635
	public function getRelatedModuleField(string $fieldName, string $moduleName)
636
	{
637
		return $this->getRelatedModuleFields($moduleName)[$fieldName] ?? null;
638
	}
639
640
	/**
641
	 * Get default custom view query.
642
	 *
643
	 * @return \App\Db\Query
644
	 */
645
	public function getDefaultCustomViewQuery()
646
	{
647
		$customView = CustomView::getInstance($this->moduleName, $this->user);
648
		$viewId = $customView->getViewId();
649
		if (empty($viewId) || 0 === $viewId) {
650
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type App\Db\Query.
Loading history...
651
		}
652
		return $this->getCustomViewQueryById($viewId);
653
	}
654
655
	/**
656 3
	 * Init function for default custom view.
657
	 *
658 3
	 * @param bool $noCache
659 3
	 * @param bool $onlyFields
660 3
	 *
661 3
	 * @return mixed
662 3
	 */
663 3
	public function initForDefaultCustomView($noCache = false, $onlyFields = false)
664
	{
665
		$customView = CustomView::getInstance($this->moduleName, $this->user);
666
		$viewId = $customView->getViewId($noCache);
667
		if (empty($viewId) || 0 === $viewId) {
668
			return false;
669
		}
670
		$this->initForCustomViewById($viewId, $onlyFields);
671
		return $viewId;
672 3
	}
673
674
	/**
675
	 * Get custom view query by id.
676
	 *
677
	 * @param int|string $viewId
678
	 *
679
	 * @return \App\Db\Query
680 3
	 */
681
	public function getCustomViewQueryById($viewId)
682 3
	{
683 3
		$this->initForCustomViewById($viewId);
684 3
		return $this->createQuery();
685 3
	}
686 3
687 3
	/**
688
	 * Add custom view fields from column.
689
	 *
690 3
	 * @param string[] $cvColumn
691
	 */
692
	private function addCustomViewFields(array $cvColumn)
693 3
	{
694
		$fieldName = $cvColumn['field_name'];
695
		$sourceFieldName = $cvColumn['source_field_name'];
696 3
		if (empty($sourceFieldName)) {
697
			if ('id' !== $fieldName) {
698
				$this->fields[] = $fieldName;
699
			}
700
		} else {
701
			$this->addRelatedField([
702
				'sourceField' => $sourceFieldName,
703
				'relatedModule' => $cvColumn['module_name'],
704 3
				'relatedField' => $fieldName,
705 2
			]);
706
		}
707 3
	}
708
709
	/**
710
	 * Get advanced conditions.
711
	 *
712
	 * @return array
713
	 */
714
	public function getAdvancedConditions(): array
715
	{
716
		return $this->advancedConditions;
717
	}
718 4
719
	/**
720 4
	 * Set advanced conditions.
721 3
	 *
722
	 * @param array $advancedConditions
723 2
	 *
724 2
	 * @return $this
725 2
	 */
726
	public function setAdvancedConditions(array $advancedConditions)
727
	{
728 2
		$this->advancedConditions = $advancedConditions;
729 2
		return $this;
730
	}
731
732
	/**
733
	 * Get custom view by id.
734
	 *
735
	 * @param mixed $viewId
736
	 * @param bool  $onlyFields
737
	 *
738 2
	 * @return $this
739
	 */
740 2
	public function initForCustomViewById($viewId, $onlyFields = false)
741 2
	{
742
		$this->fields[] = 'id';
743
		$customView = CustomView::getInstance($this->moduleName, $this->user);
744
		foreach ($customView->getColumnsListByCvid($viewId) as $cvColumn) {
745 2
			$this->addCustomViewFields($cvColumn);
746
		}
747
		foreach (CustomView::getDuplicateFields($viewId) as $fields) {
748
			$this->setSearchFieldsForDuplicates($fields['fieldname'], (bool) $fields['ignore']);
749
		}
750
		if ('Calendar' === $this->moduleName && !\in_array('activitytype', $this->fields)) {
751
			$this->fields[] = 'activitytype';
752
		} elseif ('Documents' === $this->moduleName && \in_array('filename', $this->fields)) {
753
			if (!\in_array('filelocationtype', $this->fields)) {
754
				$this->fields[] = 'filelocationtype';
755 2
			}
756
			if (!\in_array('filestatus', $this->fields)) {
757 2
				$this->fields[] = 'filestatus';
758
			}
759
		} elseif ('EmailTemplates' === $this->moduleName && !\in_array('sys_name', $this->fields)) {
760 2
			$this->fields[] = 'sys_name';
761
		}
762
		if (!$onlyFields) {
763 2
			$this->conditions = CustomView::getConditions($viewId);
764 2
			if (($customView = \App\CustomView::getCustomViewById($viewId)) && $customView['advanced_conditions']) {
765 2
				$this->setAdvancedConditions($customView['advanced_conditions']);
766
				$this->setDistinct('id');
767
			}
768 2
		}
769 2
		return $this;
770
	}
771
772
	/**
773
	 * Parse conditions to section where in query.
774
	 *
775
	 * @param array|null $conditions
776 2
	 *
777
	 * @throws \App\Exceptions\AppException
778
	 *
779
	 * @return array
780
	 */
781
	private function parseConditions(?array $conditions): array
782
	{
783
		if (empty($conditions)) {
784
			return [];
785
		}
786 2
		$where = [$conditions['condition']];
787
		foreach ($conditions['rules'] as $rule) {
788
			if (isset($rule['condition'])) {
789
				$where[] = $this->parseConditions($rule);
790
			} else {
791 2
				[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $rule['fieldname']), 3, false);
792
				if (!empty($sourceFieldName)) {
793
					$condition = $this->getRelatedCondition([
794
						'relatedModule' => $moduleName,
795
						'relatedField' => $fieldName,
796
						'sourceField' => $sourceFieldName,
797
						'value' => $rule['value'],
798
						'operator' => $rule['operator'],
799
					]);
800 4
				} else {
801
					$condition = $this->getCondition($fieldName, $rule['value'], $rule['operator']);
802 4
				}
803 4
				if ($condition) {
804 4
					$where[] = $condition;
805 4
				}
806 4
			}
807 4
		}
808 4
		return $where;
809 4
	}
810 4
811
	/**
812
	 * Parsing advanced filters conditions.
813 4
	 *
814
	 * @param mixed $advFilterList
815
	 *
816 4
	 * @return $this
817
	 */
818
	public function parseAdvFilter($advFilterList = false)
819 4
	{
820
		if (!$advFilterList) {
821 4
			$advFilterList = $this->advFilterList;
822
		}
823
		if (!$advFilterList) {
824
			return $this;
825
		}
826
		foreach ($advFilterList as $group => &$filters) {
827 4
			$and = ('and' === $group || 1 === (int) $group);
828
			if (isset($filters['columns'])) {
829 4
				$filters = $filters['columns'];
830 4
			}
831 4
			foreach ($filters as &$filter) {
832 4
				if (isset($filter['columnname'])) {
833 4
					[$tableName, $columnName, $fieldName] = array_pad(explode(':', $filter['columnname']), 3, false);
834 4
					if (empty($fieldName) && 'crmid' === $columnName && 'vtiger_crmentity' === $tableName) {
835
						$fieldName = $this->getColumnName('id');
836
					}
837 4
					$this->addCondition($fieldName, $filter['value'], $filter['comparator'], $and);
838
				} else {
839
					if (!empty($filter['source_field_name'])) {
840 4
						$this->addRelatedCondition([
841 1
							'sourceField' => $filter['source_field_name'],
842 1
							'relatedModule' => $filter['module_name'],
843
							'relatedField' => $filter['field_name'],
844
							'value' => $filter['value'],
845
							'operator' => $filter['comparator'],
846
							'conditionGroup' => $and,
847 4
						]);
848 4
					} elseif (0 === strpos($filter['field_name'], 'relationColumn_') && preg_match('/(^relationColumn_)(\d+)$/', $filter['field_name'], $matches)) {
849
						if (\in_array($matches[2], $this->advancedConditions['relationColumns'] ?? [])) {
850
							$this->advancedConditions['relationColumnsValues'][$matches[2]] = $filter;
851
						}
852
					} else {
853
						$this->addCondition($filter['field_name'], $filter['value'], $filter['comparator'], $and);
854
					}
855
				}
856
			}
857 4
		}
858
		return $this;
859 4
	}
860 3
861
	/**
862 3
	 * Create query.
863
	 *
864 4
	 * @param mixed $reBuild
865
	 *
866 4
	 * @return \App\Db\Query
867
	 */
868
	public function createQuery($reBuild = false): Db\Query
869
	{
870
		if (!$this->buildedQuery || $reBuild) {
871
			$this->query = new Db\Query();
872 4
			$this->loadSelect();
873
			$this->loadFrom();
874 4
			$this->loadWhere();
875 4
			$this->loadOrder();
876
			$this->loadJoin();
877
			$this->loadGroup();
878
			if (!empty($this->limit)) {
879
				$this->query->limit($this->limit);
880 4
			}
881
			if (!empty($this->offset)) {
882 4
				$this->query->offset($this->offset);
883 4
			}
884 4
			if (isset($this->distinct)) {
885 4
				$this->query->distinct($this->distinct);
886 4
			}
887 4
			$this->buildedQuery = $this->query;
888 3
		}
889
		return $this->buildedQuery;
890 4
	}
891 4
892
	/**
893
	 * Sets the SELECT part of the query.
894
	 */
895
	public function loadSelect()
896
	{
897
		$allFields = array_keys($this->getModuleFields());
898
		$allFields[] = 'id';
899 4
		$this->fields = array_intersect($this->fields, $allFields);
900
		$columns = [];
901
		foreach ($this->fields as &$fieldName) {
902
			if (isset($this->concatColumn[$fieldName])) {
903 4
				$columns[$fieldName] = new \yii\db\Expression($this->concatColumn[$fieldName]);
904 3
			} else {
905 3
				$columns[$fieldName] = $this->getColumnName($fieldName);
906
			}
907
		}
908 4
		foreach ($this->customColumns as $key => $customColumn) {
909 4
			if (is_numeric($key)) {
910 2
				$columns[] = $customColumn;
911
			} else {
912 4
				$columns[$key] = $customColumn;
913
			}
914 4
		}
915
		$this->query->select(array_merge($columns, $this->loadRelatedFields()));
916 4
	}
917 4
918
	/**
919
	 * Get column name by field name.
920
	 *
921
	 * @param string $fieldName
922 4
	 *
923 4
	 * @return string
924 4
	 */
925
	public function getColumnName($fieldName)
926 4
	{
927 4
		if ('id' === $fieldName) {
928 1
			$baseTable = $this->entityModel->table_name;
929 1
			return $baseTable . '.' . $this->entityModel->tab_name_index[$baseTable];
930
		}
931
		$field = $this->getModuleField($fieldName);
932 1
		return $field->getTableName() . '.' . $field->getColumnName();
933
	}
934
935 1
	/**
936 1
	 * Sets the FROM part of the query.
937
	 */
938
	public function loadFrom()
939 4
	{
940
		$this->query->from($this->entityModel->table_name);
941
	}
942
943
	/**
944
	 * Sets the JOINs part of the query.
945
	 */
946
	public function loadJoin()
947
	{
948
		$tableJoin = [];
949
		$moduleTableIndexList = $this->entityModel->tab_name_index;
950
		$baseTable = $this->entityModel->table_name;
951
		$baseTableIndex = $moduleTableIndexList[$baseTable];
952
		foreach ($this->fields as $fieldName) {
953
			if ('id' === $fieldName) {
954
				continue;
955
			}
956 4
			$field = $this->getModuleField($fieldName);
957 1
			if ('reference' === $field->getFieldDataType()) {
958 1
				$tableJoin[$field->getTableName()] = 'INNER JOIN';
959 1
				foreach ($this->referenceFields[$fieldName] as $moduleName) {
960
					if ('Users' === $moduleName && 'Users' !== $this->moduleName) {
961 4
						$this->addJoin(['LEFT JOIN', 'vtiger_users vtiger_users' . $fieldName, "{$field->getTableName()}.{$field->getColumnName()} = vtiger_users{$fieldName}.id"]);
962
						$this->addJoin(['LEFT JOIN', 'vtiger_groups vtiger_groups' . $fieldName, "{$field->getTableName()}.{$field->getColumnName()} = vtiger_groups{$fieldName}.groupid"]);
963
					}
964
				}
965
			}
966
			if (!isset($this->tablesList[$field->getTableName()])) {
967
				$this->tablesList[$field->getTableName()] = $field->getTableName();
968 4
				$tableJoin[$field->getTableName()] = $this->entityModel->getJoinClause($field->getTableName());
969
			}
970 4
		}
971 4
		foreach ($this->getEntityDefaultTableList() as $table) {
972
			if (!isset($this->tablesList[$table])) {
973
				$this->tablesList[$table] = $table;
974
			}
975
			$tableJoin[$table] = 'INNER JOIN';
976
		}
977
		if ($this->ownerFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->ownerFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
978
			//there are more than one field pointing to the users table, the real one is the one called assigned_user_id if there is one, otherwise pick the first
979 4
			if (\in_array('assigned_user_id', $this->ownerFields)) {
980
				$ownerField = 'assigned_user_id';
981 4
			} else {
982 4
				$ownerField = $this->ownerFields[0];
983
			}
984 4
		}
985 4
		foreach ($this->getEntityDefaultTableList() as $tableName) {
986 4
			$this->query->join($tableJoin[$tableName], $tableName, "$baseTable.$baseTableIndex = $tableName.{$moduleTableIndexList[$tableName]}");
987 3
			unset($this->tablesList[$tableName]);
988
		}
989
		unset($this->tablesList[$baseTable]);
990
		foreach ($this->tablesList as $tableName) {
991 3
			$joinType = $tableJoin[$tableName] ?? $this->entityModel->getJoinClause($tableName);
992
			if ('vtiger_users' === $tableName) {
993
				$field = $this->getModuleField($ownerField);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ownerField does not seem to be defined for all execution paths leading up to this point.
Loading history...
994 4
				$this->addJoin([$joinType, $tableName, "{$field->getTableName()}.{$field->getColumnName()} = $tableName.id"]);
995
			} elseif ('vtiger_groups' == $tableName) {
996
				$field = $this->getModuleField($ownerField);
997
				$this->addJoin([$joinType, $tableName, "{$field->getTableName()}.{$field->getColumnName()} = $tableName.groupid"]);
998
			} elseif (isset($moduleTableIndexList[$tableName])) {
999
				$this->addJoin([$joinType, $tableName, "$baseTable.$baseTableIndex = $tableName.$moduleTableIndexList[$tableName]"]);
1000
			}
1001 4
		}
1002
		if ($this->searchFieldsForDuplicates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->searchFieldsForDuplicates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1003 4
			$duplicateCheckClause = [];
1004 4
            $queryGenerator = new self($this->moduleName, $this->user->getId());
1005 4
            $queryGenerator->setStateCondition($this->getState());
1006 2
			$queryGenerator->permissions = $this->permissions;
1007 2
			$queryGenerator->setFields(array_keys($this->searchFieldsForDuplicates));
1008 3
			foreach ($this->searchFieldsForDuplicates as $fieldName => $ignoreEmptyValue) {
1009
				if ($ignoreEmptyValue) {
1010
					$queryGenerator->addCondition($fieldName, '', 'ny');
1011
				}
1012 3
				$queryGenerator->setGroup($fieldName);
1013
				$fieldModel = $this->getModuleField($fieldName);
1014 4
				$duplicateCheckClause[] = $fieldModel->getTableName() . '.' . $fieldModel->getColumnName() . ' = duplicates.' . $fieldModel->getFieldName();
0 ignored issues
show
Deprecated Code introduced by
The function Vtiger_Field_Model::getFieldName() has been deprecated: Use $this->getName() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1014
				$duplicateCheckClause[] = $fieldModel->getTableName() . '.' . $fieldModel->getColumnName() . ' = duplicates.' . /** @scrutinizer ignore-deprecated */ $fieldModel->getFieldName();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1015
			}
1016
			$subQuery = $queryGenerator->createQuery();
1017
			$subQuery->andHaving((new \yii\db\Expression('COUNT(1) > 1')));
1018
			$this->joins['duplicates'] = ['INNER JOIN', ['duplicates' => $subQuery], implode(' AND ', $duplicateCheckClause)];
1019
		}
1020
		uksort($this->joins, fn ($a, $b) => (int) (!isset($moduleTableIndexList[$a]) && isset($moduleTableIndexList[$b])));
1021
		foreach ($this->joins as $join) {
1022
			$on = $join[2] ?? '';
1023
			$params = $join[3] ?? [];
1024
			$this->query->join($join[0], $join[1], $on, $params);
1025
		}
1026
	}
1027
1028
	/**
1029
	 * Get entity default table list.
1030
	 *
1031
	 * @return type
0 ignored issues
show
Bug introduced by
The type App\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1032
	 */
1033
	public function getEntityDefaultTableList()
1034
	{
1035
		if (isset($this->entityModel->tab_name_index['vtiger_crmentity'])) {
1036
			return ['vtiger_crmentity'];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('vtiger_crmentity') returns the type array<integer,string> which is incompatible with the documented return type App\type.
Loading history...
1037
		}
1038
		return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type App\type.
Loading history...
1039
	}
1040
1041
	/**
1042
	 * Sets the WHERE part of the query.
1043
	 */
1044
	public function loadWhere()
1045
	{
1046
		if (null !== $this->stateCondition) {
1047
			$this->query->andWhere($this->getStateCondition());
1048
		}
1049
		if ($this->advancedConditions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->advancedConditions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1050
			$this->loadAdvancedConditions();
1051
		}
1052
		$this->query->andWhere(['and', array_merge(['and'], $this->conditionsAnd), array_merge(['or'], $this->conditionsOr)]);
1053 4
		$this->query->andWhere($this->parseConditions($this->conditions));
1054
		if ($this->permissions) {
1055 4
			if (\App\Config::security('CACHING_PERMISSION_TO_RECORD') && 'Users' !== $this->moduleName) {
1056 4
				$userId = $this->user->getId();
1057
				$this->query->andWhere(['like', 'vtiger_crmentity.users', ",$userId,"]);
1058
			} else {
1059 4
				PrivilegeQuery::getConditions($this->query, $this->moduleName, $this->user, $this->sourceRecord);
0 ignored issues
show
Bug introduced by
$this->user of type App\User is incompatible with the type integer expected by parameter $user of App\PrivilegeQuery::getConditions(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1059
				PrivilegeQuery::getConditions($this->query, $this->moduleName, /** @scrutinizer ignore-type */ $this->user, $this->sourceRecord);
Loading history...
1060 4
			}
1061 4
		}
1062 4
	}
1063 4
1064
	/**
1065 4
	 * Load advanced conditions to section where in query.
1066
	 *
1067
	 * @return void
1068
	 */
1069
	private function loadAdvancedConditions(): void
1070
	{
1071
		if (!empty($this->advancedConditions['relationId']) && ($relationModel = \Vtiger_Relation_Model::getInstanceById($this->advancedConditions['relationId']))) {
1072
			$typeRelationModel = $relationModel->getTypeRelationModel();
1073
			if (!method_exists($typeRelationModel, 'loadAdvancedConditionsByRelationId')) {
1074
				$className = \get_class($typeRelationModel);
1075
				Log::error("The relationship relationId: {$this->advancedConditions['relationId']} does not support advanced conditions | No function in the class: $className | Module: " . $this->getModule());
1076
				throw new \App\Exceptions\AppException("ERR_FUNCTION_NOT_FOUND_IN_CLASS||loadAdvancedConditionsByRelationId|$className|" . $this->getModule());
1077
			}
1078
			$typeRelationModel->loadAdvancedConditionsByRelationId($this);
1079
		}
1080
		if (!empty($this->advancedConditions['relationColumnsValues'])) {
1081
			foreach ($this->advancedConditions['relationColumnsValues'] as $relationId => $value) {
1082
				if ($relationModel = \Vtiger_Relation_Model::getInstanceById($relationId)) {
1083
					$typeRelationModel = $relationModel->getTypeRelationModel();
1084
					if (!method_exists($typeRelationModel, 'loadAdvancedConditionsByColumns')) {
1085
						$className = \get_class($typeRelationModel);
1086
						Log::error("The relationship relationId: {$relationId} does not support advanced conditions | No function in the class: $className | Module: " . $this->getModule());
1087
						throw new \App\Exceptions\AppException("ERR_FUNCTION_NOT_FOUND_IN_CLASS|loadAdvancedConditionsByColumns|$className|" . $this->getModule());
1088
					}
1089
					$typeRelationModel->loadAdvancedConditionsByColumns($this, $value);
1090
				}
1091
			}
1092
		}
1093
	}
1094
1095
	/**
1096
	 * Get records state.
1097
	 *
1098
	 * @return string
1099
	 */
1100
	public function getState(): string
1101
	{
1102
		if (null === $this->stateCondition) {
1103
			return 'All';
1104 2
		}
1105
		switch ($this->stateCondition) {
1106 2
			default:
1107 2
			case 0:
1108 2
				$stateCondition = 'Active';
1109 2
				break;
1110
			case 1:
1111
				$stateCondition = 'Trash';
1112
				break;
1113
			case 2:
1114
				$stateCondition = 'Archived';
1115
				break;
1116 2
		}
1117
		return $stateCondition;
1118
	}
1119
1120
	/**
1121
	 * Get conditions for records state.
1122
	 *
1123
	 * @return array|string
1124
	 */
1125
	private function getStateCondition()
1126
	{
1127
		$condition = ['vtiger_crmentity.deleted' => $this->stateCondition];
1128 4
		switch ($this->moduleName) {
1129
			case 'Leads':
1130 4
				$condition += ['vtiger_leaddetails.converted' => 0];
1131 3
				break;
1132
			case 'Users':
1133 4
				$condition = [];
1134
				break;
1135
			default:
1136
				break;
1137
		}
1138 4
		return $condition;
1139 4
	}
1140
1141
	/**
1142
	 * Set state condition.
1143 4
	 *
1144 4
	 * @param string $state
1145
	 *
1146
	 * @return $this
1147
	 */
1148 4
	public function setStateCondition($state)
1149 4
	{
1150
		switch ($state) {
1151
			default:
1152
			case 'Active':
1153
				$this->stateCondition = 0;
1154
				break;
1155
			case 'Trash':
1156
				$this->stateCondition = 1;
1157
				break;
1158
			case 'Archived':
1159
				$this->stateCondition = 2;
1160
				break;
1161
			case 'All':
1162
				$this->stateCondition = null;
1163
				break;
1164
		}
1165
		return $this;
1166
	}
1167
1168
	/**
1169
	 * Returns condition for field in this module.
1170
	 *
1171
	 * @param string $fieldName
1172
	 * @param mixed  $value
1173
	 * @param string $operator
1174
	 * @param bool   $userFormat
1175
	 *
1176
	 * @throws \App\Exceptions\AppException
1177
	 *
1178
	 * @return array|bool
1179
	 */
1180
	private function getCondition(string $fieldName, $value, string $operator, bool $userFormat = false)
1181
	{
1182
		$queryField = $this->getQueryField($fieldName);
1183
		if ($userFormat && $queryField->getField()) {
1184
			$value = $queryField->getField()->getUITypeModel()->getDbConditionBuilderValue($value, $operator);
1185
		}
1186
		$queryField->setValue($value);
1187
		$queryField->setOperator($operator);
1188
		$condition = $queryField->getCondition();
1189
		if ($condition && ($field = $this->getModuleField($fieldName)) && !isset($this->tablesList[$field->getTableName()])) {
1190
			$this->tablesList[$field->getTableName()] = $field->getTableName();
1191
		}
1192
		return $condition;
1193
	}
1194
1195
	/**
1196
	 * Returns condition for field in related module.
1197
	 *
1198
	 * @param array $condition
1199
	 *
1200
	 * @throws \App\Exceptions\AppException
1201
	 *
1202
	 * @return array|bool
1203
	 */
1204
	private function getRelatedCondition(array $condition)
1205
	{
1206
		$field = $this->addRelatedJoin($condition);
1207
		if (!$field) {
1208
			Log::error('Not found source field', __METHOD__);
1209
			return false;
1210
		}
1211
		$queryField = $this->getQueryRelatedField($condition, $field);
1212
		$queryField->setValue($condition['value']);
1213
		$queryField->setOperator($condition['operator']);
1214
		return $queryField->getCondition();
1215
	}
1216
1217
	/**
1218
	 * Set condition.
1219
	 *
1220
	 * @param string $fieldName
1221
	 * @param mixed  $value
1222
	 * @param string $operator
1223
	 * @param mixed  $groupAnd
1224
	 * @param bool   $userFormat
1225
	 *
1226
	 * @see Condition::ADVANCED_FILTER_OPTIONS
1227
	 * @see Condition::DATE_OPERATORS
1228
	 *
1229
	 * @return $this
1230
	 */
1231
	public function addCondition($fieldName, $value, $operator, $groupAnd = true, $userFormat = false)
1232
	{
1233
		$condition = $this->getCondition($fieldName, $value, $operator, $userFormat);
1234
		if ($condition) {
1235
			if ($groupAnd) {
1236
				$this->conditionsAnd[] = $condition;
1237
			} else {
1238
				$this->conditionsOr[] = $condition;
1239
			}
1240
		} else {
1241
			Log::error('Wrong condition');
1242
		}
1243
		return $this;
1244
	}
1245
1246
	/**
1247 4
	 * Get query field instance.
1248
	 *
1249 4
	 * @param string $fieldName
1250
	 *
1251
	 * @throws \App\Exceptions\AppException
1252 4
	 *
1253
	 * @return \App\Conditions\QueryFields\BaseField
1254
	 */
1255
	public function getQueryField($fieldName)
1256
	{
1257 4
		if (isset($this->queryFields[$fieldName])) {
1258
			return $this->queryFields[$fieldName];
1259 4
		}
1260
		if ('id' === $fieldName) {
1261
			$queryField = new Conditions\QueryFields\IdField($this, '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type Vtiger_Field_Model expected by parameter $fieldModel of App\Conditions\QueryFields\IdField::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1261
			$queryField = new Conditions\QueryFields\IdField($this, /** @scrutinizer ignore-type */ '');
Loading history...
1262 4
			return $this->queryFields[$fieldName] = $queryField;
1263
		}
1264
		$field = $this->getModuleField($fieldName);
1265
		if (empty($field)) {
1266
			Log::error("Not found field model | Field name: '$fieldName' in module" . $this->getModule());
1267
			throw new \App\Exceptions\AppException("ERR_NOT_FOUND_FIELD_MODEL|$fieldName|" . $this->getModule());
1268
		}
1269
		$className = '\App\Conditions\QueryFields\\' . ucfirst($field->getFieldDataType()) . 'Field';
1270
		if (!class_exists($className)) {
1271 2
			Log::error('Not found query field condition | FieldDataType: ' . ucfirst($field->getFieldDataType()));
1272
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_QUERY_FIELD_CONDITION|' . $fieldName);
1273 2
		}
1274
		$queryField = new $className($this, $field);
1275
		return $this->queryFields[$fieldName] = $queryField;
1276 2
	}
1277 2
1278 2
	/**
1279 2
	 * Set condition on reference module fields.
1280 2
	 *
1281
	 * @param array $condition
1282
	 */
1283 2
	public function addRelatedCondition($condition)
1284 2
	{
1285 2
		$queryCondition = $this->getRelatedCondition($condition);
1286 2
		if ($queryCondition) {
1287 2
			if ($condition['conditionGroup']) {
1288 2
				$this->conditionsAnd[] = $queryCondition;
1289 2
			} else {
1290
				$this->conditionsOr[] = $queryCondition;
1291
			}
1292 2
		} else {
1293
			Log::error('Wrong condition');
1294 2
		}
1295
	}
1296
1297 2
	/**
1298
	 * Set related field join.
1299
	 *
1300
	 * @param string[] $fieldDetail
1301
	 *
1302
	 * @return bool|\Vtiger_Field_Model
1303
	 */
1304
	public function addRelatedJoin($fieldDetail)
1305
	{
1306
		$relatedFieldModel = $this->getRelatedModuleField($fieldDetail['relatedField'], $fieldDetail['relatedModule']);
1307
		if (!$relatedFieldModel || !$relatedFieldModel->isActiveField()) {
0 ignored issues
show
introduced by
$relatedFieldModel is of type Vtiger_Field_Model, thus it always evaluated to true.
Loading history...
1308
			Log::warning("Field in related module is inactive or does not exist. Related module: {$fieldDetail['relatedModule']} | Related field: {$fieldDetail['relatedField']}");
1309
			return false;
1310
		}
1311
		$tableName = $relatedFieldModel->getTableName();
1312
		$sourceFieldModel = $this->getModuleField($fieldDetail['sourceField']);
1313
		$relatedTableName = $tableName . $fieldDetail['sourceField'];
1314
		$relatedTableIndex = $relatedFieldModel->getModule()->getEntityInstance()->tab_name_index[$tableName];
1315
		$this->addJoin(['LEFT JOIN', "$tableName $relatedTableName", "{$sourceFieldModel->getTableName()}.{$sourceFieldModel->getColumnName()} = $relatedTableName.$relatedTableIndex"]);
1316 2
		return $relatedFieldModel;
1317
	}
1318
1319 2
	/**
1320 2
	 * Get query related field instance.
1321
	 *
1322 2
	 * @param array|string        $relatedInfo
1323
	 * @param \Vtiger_Field_Model $field
1324
	 *
1325
	 * @throws \App\Exceptions\AppException
1326
	 *
1327
	 * @return \App\Conditions\QueryFields\BaseField
1328
	 */
1329
	public function getQueryRelatedField($relatedInfo, ?\Vtiger_Field_Model $field = null)
1330
	{
1331
		if (!\is_array($relatedInfo)) {
1332
			[$fieldName, $relatedModule, $sourceFieldName] = array_pad(explode(':', $relatedInfo), 3, false);
1333
			$relatedInfo = [
1334
				'sourceField' => $sourceFieldName,
1335
				'relatedModule' => $relatedModule,
1336
				'relatedField' => $fieldName,
1337
			];
1338
		}
1339
		$relatedModule = $relatedInfo['relatedModule'];
1340
		$fieldName = $relatedInfo['relatedField'];
1341
1342
		if (isset($this->relatedQueryFields[$relatedModule][$fieldName])) {
1343
			$queryField = clone $this->relatedQueryFields[$relatedModule][$fieldName];
1344
			$queryField->setRelated($relatedInfo);
1345
			return $queryField;
1346
		}
1347
		if (null === $field) {
1348
			$field = $this->getRelatedModuleField($fieldName, $relatedModule);
1349
		}
1350
		$className = '\App\Conditions\QueryFields\\' . ucfirst($field->getFieldDataType()) . 'Field';
1351
		if (!class_exists($className)) {
1352
			Log::error('Not found query field condition');
1353
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_QUERY_FIELD_CONDITION');
1354
		}
1355
		$queryField = new $className($this, $field);
1356
		$queryField->setRelated($relatedInfo);
1357
		return $this->relatedQueryFields[$relatedModule][$field->getName()] = $queryField;
1358
	}
1359
1360
	/**
1361
	 * Set order for related module.
1362
	 *
1363
	 * @param string[] $orderDetail
1364
	 */
1365
	public function setRelatedOrder(array $orderDetail)
1366
	{
1367
		$field = $this->addRelatedJoin($orderDetail);
1368
		if (!$field) {
1369
			Log::error('Not found source field');
1370
		}
1371
		$queryField = $this->getQueryRelatedField($orderDetail, $field);
1372
		$this->order = array_merge($this->order, $queryField->getOrderBy($orderDetail['relatedSortOrder']));
1373
	}
1374
1375
	/**
1376
	 * Sets the ORDER BY part of the query.
1377
	 */
1378
	public function loadOrder()
1379
	{
1380
		if ($this->order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->order of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1381
			$this->query->orderBy($this->order);
1382
		}
1383
	}
1384
1385
	/**
1386
	 * Sets the GROUP BY part of the query.
1387
	 */
1388
	public function loadGroup()
1389
	{
1390
		if ($this->group) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->group of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1391
			$this->query->groupBy(array_unique($this->group));
1392
		}
1393
	}
1394
1395
	/**
1396
	 * Parse base search condition to db condition.
1397
	 *
1398
	 * @param array $searchParams Example: [[["firstname","a","Tom"]]]
1399
	 *
1400
	 * @return array
1401
	 */
1402
	public function parseBaseSearchParamsToCondition($searchParams)
1403
	{
1404
		if (empty($searchParams)) {
1405
			return [];
1406
		}
1407
		$advFilterConditionFormat = [];
1408
		$glueOrder = ['and', 'or'];
1409
		$groupIterator = 0;
1410
		foreach ($searchParams as $groupInfo) {
1411
			if (!empty($groupInfo)) {
1412
				$groupColumnsInfo = [];
1413
				foreach ($groupInfo as $fieldSearchInfo) {
1414
					if ($fieldSearchInfo) {
1415
						[$fieldNameInfo, $operator, $fieldValue] = array_pad($fieldSearchInfo, 3, false);
1416
						$fieldValue = Purifier::decodeHtml($fieldValue);
1417
						[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $fieldNameInfo), 3, false);
1418
						if (!empty($sourceFieldName)) {
1419
							$field = $this->getRelatedModuleField($fieldName, $moduleName);
1420
						} else {
1421
							$field = $this->getModuleField($fieldName);
1422
						}
1423
						if ($field && ('date_start' === $fieldName || 'due_date' === $fieldName || 'datetime' === $field->getFieldDataType())) {
1424
							$dateValues = explode(',', $fieldValue);
1425
							//Indicate whether it is fist date in the between condition
1426
							$isFirstDate = true;
1427
							foreach ($dateValues as $key => $dateValue) {
1428
								$dateTimeCompoenents = explode(' ', $dateValue);
1429
								if (empty($dateTimeCompoenents[1])) {
1430
									if ($isFirstDate) {
1431
										$dateTimeCompoenents[1] = '00:00:00';
1432
									} else {
1433
										$dateTimeCompoenents[1] = '23:59:59';
1434
									}
1435
								}
1436
								$dateValue = implode(' ', $dateTimeCompoenents);
1437
								$dateValues[$key] = $dateValue;
1438
								$isFirstDate = false;
1439
							}
1440
							$fieldValue = implode(',', $dateValues);
1441
						}
1442
						$groupColumnsInfo[] = ['field_name' => $fieldName, 'module_name' => $moduleName, 'source_field_name' => $sourceFieldName, 'comparator' => $operator, 'value' => $fieldValue];
1443
					}
1444
				}
1445
				$advFilterConditionFormat[$glueOrder[$groupIterator]] = $groupColumnsInfo;
1446
			}
1447
			++$groupIterator;
1448
		}
1449
		return $advFilterConditionFormat;
1450
	}
1451
1452
	/**
1453
	 * Parse search condition to standard condition.
1454
	 *
1455
	 * @param array $searchParams
1456
	 *
1457
	 * @return array
1458
	 */
1459
	public function parseSearchParams(array $searchParams): array
1460
	{
1461
		$glueOrder = ['AND', 'OR'];
1462
		$searchParamsConditions = [];
1463
		foreach ($searchParams as $key => $conditions) {
1464
			if (empty($conditions)) {
1465
				continue;
1466
			}
1467
			$searchParamsConditions['condition'] = $glueOrder[$key];
1468
			$searchParamsConditions['rules'] = [];
1469
			foreach ($conditions as $condition) {
1470
				[$fieldName, , $sourceFieldName] = array_pad(explode(':', $condition[0]), 3, false);
1471
				if (!$sourceFieldName) {
1472
					$condition[0] = "{$fieldName}:{$this->getModule()}";
1473
				}
1474
				$searchParamsConditions['rules'][] = ['fieldname' => $condition[0], 'operator' => $condition[1], 'value' => $condition[2]];
1475
			}
1476
		}
1477
		return $searchParamsConditions;
1478
	}
1479
}
1480