Passed
Push — developer ( 762b64...a1cff0 )
by Radosław
20:19
created

QueryGenerator::getQueryInvField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 15
ccs 3
cts 3
cp 1
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
1
<?php
2
/**
3
 * Query generator file.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 5.0 (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 ('INVENTORY' === $sourceFieldName) {
793
					$condition = $this->getInvCondition($fieldName, $rule['value'], $rule['operator']);
794
				} elseif (!empty($sourceFieldName)) {
795
					$condition = $this->getRelatedCondition([
796
						'relatedModule' => $moduleName,
797
						'relatedField' => $fieldName,
798
						'sourceField' => $sourceFieldName,
799
						'value' => $rule['value'],
800 4
						'operator' => $rule['operator'],
801
					]);
802 4
				} else {
803 4
					$condition = $this->getCondition($fieldName, $rule['value'], $rule['operator']);
804 4
				}
805 4
				if ($condition) {
806 4
					$where[] = $condition;
807 4
				}
808 4
			}
809 4
		}
810 4
		return $where;
811
	}
812
813 4
	/**
814
	 * Parsing advanced filters conditions.
815
	 *
816 4
	 * @param mixed $advFilterList
817
	 *
818
	 * @return $this
819 4
	 */
820
	public function parseAdvFilter($advFilterList = false)
821 4
	{
822
		if (!$advFilterList) {
823
			$advFilterList = $this->advFilterList;
824
		}
825
		if (!$advFilterList) {
826
			return $this;
827 4
		}
828
		foreach ($advFilterList as $group => &$filters) {
829 4
			$and = ('and' === $group || 1 === (int) $group);
830 4
			if (isset($filters['columns'])) {
831 4
				$filters = $filters['columns'];
832 4
			}
833 4
			foreach ($filters as &$filter) {
834 4
				if (isset($filter['columnname'])) {
835
					[$tableName, $columnName, $fieldName] = array_pad(explode(':', $filter['columnname']), 3, false);
836
					if (empty($fieldName) && 'crmid' === $columnName && 'vtiger_crmentity' === $tableName) {
837 4
						$fieldName = $this->getColumnName('id');
838
					}
839
					$this->addCondition($fieldName, $filter['value'], $filter['comparator'], $and);
840 4
				} else {
841 1
					if (!empty($filter['source_field_name'])) {
842 1
						$this->addRelatedCondition([
843
							'sourceField' => $filter['source_field_name'],
844
							'relatedModule' => $filter['module_name'],
845
							'relatedField' => $filter['field_name'],
846
							'value' => $filter['value'],
847 4
							'operator' => $filter['comparator'],
848 4
							'conditionGroup' => $and,
849
						]);
850
					} elseif (0 === strpos($filter['field_name'], 'relationColumn_') && preg_match('/(^relationColumn_)(\d+)$/', $filter['field_name'], $matches)) {
851
						if (\in_array($matches[2], $this->advancedConditions['relationColumns'] ?? [])) {
852
							$this->advancedConditions['relationColumnsValues'][$matches[2]] = $filter;
853
						}
854
					} else {
855
						$this->addCondition($filter['field_name'], $filter['value'], $filter['comparator'], $and);
856
					}
857 4
				}
858
			}
859 4
		}
860 3
		return $this;
861
	}
862 3
863
	/**
864 4
	 * Create query.
865
	 *
866 4
	 * @param mixed $reBuild
867
	 *
868
	 * @return \App\Db\Query
869
	 */
870
	public function createQuery($reBuild = false): Db\Query
871
	{
872 4
		if (!$this->buildedQuery || $reBuild) {
873
			$this->query = new Db\Query();
874 4
			$this->loadSelect();
875 4
			$this->loadFrom();
876
			$this->loadWhere();
877
			$this->loadOrder();
878
			$this->loadJoin();
879
			$this->loadGroup();
880 4
			if (!empty($this->limit)) {
881
				$this->query->limit($this->limit);
882 4
			}
883 4
			if (!empty($this->offset)) {
884 4
				$this->query->offset($this->offset);
885 4
			}
886 4
			if (isset($this->distinct)) {
887 4
				$this->query->distinct($this->distinct);
888 3
			}
889
			$this->buildedQuery = $this->query;
890 4
		}
891 4
		return $this->buildedQuery;
892
	}
893
894
	/**
895
	 * Sets the SELECT part of the query.
896
	 */
897
	public function loadSelect()
898
	{
899 4
		$allFields = array_keys($this->getModuleFields());
900
		$allFields[] = 'id';
901
		$this->fields = array_intersect($this->fields, $allFields);
902
		$columns = [];
903 4
		foreach ($this->fields as &$fieldName) {
904 3
			if (isset($this->concatColumn[$fieldName])) {
905 3
				$columns[$fieldName] = new \yii\db\Expression($this->concatColumn[$fieldName]);
906
			} else {
907
				$columns[$fieldName] = $this->getColumnName($fieldName);
908 4
			}
909 4
		}
910 2
		foreach ($this->customColumns as $key => $customColumn) {
911
			if (is_numeric($key)) {
912 4
				$columns[] = $customColumn;
913
			} else {
914 4
				$columns[$key] = $customColumn;
915
			}
916 4
		}
917 4
		$this->query->select(array_merge($columns, $this->loadRelatedFields()));
918
	}
919
920
	/**
921
	 * Get column name by field name.
922 4
	 *
923 4
	 * @param string $fieldName
924 4
	 *
925
	 * @return string
926 4
	 */
927 4
	public function getColumnName($fieldName)
928 1
	{
929 1
		if ('id' === $fieldName) {
930
			$baseTable = $this->entityModel->table_name;
931
			return $baseTable . '.' . $this->entityModel->tab_name_index[$baseTable];
932 1
		}
933
		$field = $this->getModuleField($fieldName);
934
		return $field->getTableName() . '.' . $field->getColumnName();
935 1
	}
936 1
937
	/**
938
	 * Sets the FROM part of the query.
939 4
	 */
940
	public function loadFrom()
941
	{
942
		$this->query->from($this->entityModel->table_name);
943
	}
944
945
	/**
946
	 * Sets the JOINs part of the query.
947
	 */
948
	public function loadJoin()
949
	{
950
		$tableJoin = [];
951
		$moduleTableIndexList = $this->entityModel->tab_name_index;
952
		$baseTable = $this->entityModel->table_name;
953
		$baseTableIndex = $moduleTableIndexList[$baseTable];
954
		foreach ($this->fields as $fieldName) {
955
			if ('id' === $fieldName) {
956 4
				continue;
957 1
			}
958 1
			$field = $this->getModuleField($fieldName);
959 1
			if ('reference' === $field->getFieldDataType()) {
960
				$tableJoin[$field->getTableName()] = 'INNER JOIN';
961 4
				foreach ($this->referenceFields[$fieldName] as $moduleName) {
962
					if ('Users' === $moduleName && 'Users' !== $this->moduleName) {
963
						$this->addJoin(['LEFT JOIN', 'vtiger_users vtiger_users' . $fieldName, "{$field->getTableName()}.{$field->getColumnName()} = vtiger_users{$fieldName}.id"]);
964
						$this->addJoin(['LEFT JOIN', 'vtiger_groups vtiger_groups' . $fieldName, "{$field->getTableName()}.{$field->getColumnName()} = vtiger_groups{$fieldName}.groupid"]);
965
					}
966
				}
967
			}
968 4
			if (!isset($this->tablesList[$field->getTableName()])) {
969
				$this->tablesList[$field->getTableName()] = $field->getTableName();
970 4
				$tableJoin[$field->getTableName()] = $this->entityModel->getJoinClause($field->getTableName());
971 4
			}
972
		}
973
		foreach ($this->getEntityDefaultTableList() as $table) {
974
			if (!isset($this->tablesList[$table])) {
975
				$this->tablesList[$table] = $table;
976
			}
977
			$tableJoin[$table] = 'INNER JOIN';
978
		}
979 4
		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...
980
			//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
981 4
			if (\in_array('assigned_user_id', $this->ownerFields)) {
982 4
				$ownerField = 'assigned_user_id';
983
			} else {
984 4
				$ownerField = $this->ownerFields[0];
985 4
			}
986 4
		}
987 3
		foreach ($this->getEntityDefaultTableList() as $tableName) {
988
			$this->query->join($tableJoin[$tableName], $tableName, "$baseTable.$baseTableIndex = $tableName.{$moduleTableIndexList[$tableName]}");
989
			unset($this->tablesList[$tableName]);
990
		}
991 3
		unset($this->tablesList[$baseTable]);
992
		foreach ($this->tablesList as $tableName) {
993
			$joinType = $tableJoin[$tableName] ?? $this->entityModel->getJoinClause($tableName);
994 4
			if ('vtiger_users' === $tableName) {
995
				$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...
996
				$this->addJoin([$joinType, $tableName, "{$field->getTableName()}.{$field->getColumnName()} = $tableName.id"]);
997
			} elseif ('vtiger_groups' == $tableName) {
998
				$field = $this->getModuleField($ownerField);
999
				$this->addJoin([$joinType, $tableName, "{$field->getTableName()}.{$field->getColumnName()} = $tableName.groupid"]);
1000
			} elseif (isset($moduleTableIndexList[$tableName])) {
1001 4
				$this->addJoin([$joinType, $tableName, "$baseTable.$baseTableIndex = $tableName.$moduleTableIndexList[$tableName]"]);
1002
			}
1003 4
		}
1004 4
		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...
1005 4
			$duplicateCheckClause = [];
1006 2
			$queryGenerator = new self($this->moduleName, $this->user->getId());
1007 2
			$queryGenerator->permissions = $this->permissions;
1008 3
			$queryGenerator->setFields(array_keys($this->searchFieldsForDuplicates));
1009
			foreach ($this->searchFieldsForDuplicates as $fieldName => $ignoreEmptyValue) {
1010
				if ($ignoreEmptyValue) {
1011
					$queryGenerator->addCondition($fieldName, '', 'ny');
1012 3
				}
1013
				$queryGenerator->setGroup($fieldName);
1014 4
				$fieldModel = $this->getModuleField($fieldName);
1015
				$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

1015
				$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...
1016
			}
1017
			$subQuery = $queryGenerator->createQuery();
1018
			$subQuery->andHaving((new \yii\db\Expression('COUNT(1) > 1')));
1019
			$this->joins['duplicates'] = ['INNER JOIN', ['duplicates' => $subQuery], implode(' AND ', $duplicateCheckClause)];
1020
		}
1021
		uksort($this->joins, fn ($a, $b) => (int) (!isset($moduleTableIndexList[$a]) && isset($moduleTableIndexList[$b])));
1022
		foreach ($this->joins as $join) {
1023
			$on = $join[2] ?? '';
1024
			$params = $join[3] ?? [];
1025
			$this->query->join($join[0], $join[1], $on, $params);
1026
		}
1027
	}
1028
1029
	/**
1030
	 * Get entity default table list.
1031
	 *
1032
	 * @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...
1033
	 */
1034
	public function getEntityDefaultTableList()
1035
	{
1036
		if (isset($this->entityModel->tab_name_index['vtiger_crmentity'])) {
1037
			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...
1038
		}
1039
		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...
1040
	}
1041
1042
	/**
1043
	 * Sets the WHERE part of the query.
1044
	 */
1045
	public function loadWhere()
1046
	{
1047
		if (null !== $this->stateCondition) {
1048
			$this->query->andWhere($this->getStateCondition());
1049
		}
1050
		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...
1051
			$this->loadAdvancedConditions();
1052
		}
1053 4
		$this->query->andWhere(['and', array_merge(['and'], $this->conditionsAnd), array_merge(['or'], $this->conditionsOr)]);
1054
		$this->query->andWhere($this->parseConditions($this->conditions));
1055 4
		if ($this->permissions) {
1056 4
			if (\App\Config::security('CACHING_PERMISSION_TO_RECORD') && 'Users' !== $this->moduleName) {
1057
				$userId = $this->user->getId();
1058
				$this->query->andWhere(['like', 'vtiger_crmentity.users', ",$userId,"]);
1059 4
			} else {
1060 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

1060
				PrivilegeQuery::getConditions($this->query, $this->moduleName, /** @scrutinizer ignore-type */ $this->user, $this->sourceRecord);
Loading history...
1061 4
			}
1062 4
		}
1063 4
	}
1064
1065 4
	/**
1066
	 * Load advanced conditions to section where in query.
1067
	 *
1068
	 * @return void
1069
	 */
1070
	private function loadAdvancedConditions(): void
1071
	{
1072
		if (!empty($this->advancedConditions['relationId']) && ($relationModel = \Vtiger_Relation_Model::getInstanceById($this->advancedConditions['relationId']))) {
1073
			$typeRelationModel = $relationModel->getTypeRelationModel();
1074
			if (!method_exists($typeRelationModel, 'loadAdvancedConditionsByRelationId')) {
1075
				$className = \get_class($typeRelationModel);
1076
				Log::error("The relationship relationId: {$this->advancedConditions['relationId']} does not support advanced conditions | No function in the class: $className | Module: " . $this->getModule());
1077
				throw new \App\Exceptions\AppException("ERR_FUNCTION_NOT_FOUND_IN_CLASS||loadAdvancedConditionsByRelationId|$className|" . $this->getModule());
1078
			}
1079
			$typeRelationModel->loadAdvancedConditionsByRelationId($this);
1080
		}
1081
		if (!empty($this->advancedConditions['relationColumnsValues'])) {
1082
			foreach ($this->advancedConditions['relationColumnsValues'] as $relationId => $value) {
1083
				if ($relationModel = \Vtiger_Relation_Model::getInstanceById($relationId)) {
1084
					$typeRelationModel = $relationModel->getTypeRelationModel();
1085
					if (!method_exists($typeRelationModel, 'loadAdvancedConditionsByColumns')) {
1086
						$className = \get_class($typeRelationModel);
1087
						Log::error("The relationship relationId: {$relationId} does not support advanced conditions | No function in the class: $className | Module: " . $this->getModule());
1088
						throw new \App\Exceptions\AppException("ERR_FUNCTION_NOT_FOUND_IN_CLASS|loadAdvancedConditionsByColumns|$className|" . $this->getModule());
1089
					}
1090
					$typeRelationModel->loadAdvancedConditionsByColumns($this, $value);
1091
				}
1092
			}
1093
		}
1094
	}
1095
1096
	/**
1097
	 * Get records state.
1098
	 *
1099
	 * @return string
1100
	 */
1101
	public function getState(): string
1102
	{
1103
		if (null === $this->stateCondition) {
1104 2
			return 'All';
1105
		}
1106 2
		switch ($this->stateCondition) {
1107 2
			default:
1108 2
			case 0:
1109 2
				$stateCondition = 'Active';
1110
				break;
1111
			case 1:
1112
				$stateCondition = 'Trash';
1113
				break;
1114
			case 2:
1115
				$stateCondition = 'Archived';
1116 2
				break;
1117
		}
1118
		return $stateCondition;
1119
	}
1120
1121
	/**
1122
	 * Get conditions for records state.
1123
	 *
1124
	 * @return array|string
1125
	 */
1126
	private function getStateCondition()
1127
	{
1128 4
		$condition = ['vtiger_crmentity.deleted' => $this->stateCondition];
1129
		switch ($this->moduleName) {
1130 4
			case 'Leads':
1131 3
				$condition += ['vtiger_leaddetails.converted' => 0];
1132
				break;
1133 4
			case 'Users':
1134
				$condition = [];
1135
				break;
1136
			default:
1137
				break;
1138 4
		}
1139 4
		return $condition;
1140
	}
1141
1142
	/**
1143 4
	 * Set state condition.
1144 4
	 *
1145
	 * @param string $state
1146
	 *
1147
	 * @return $this
1148 4
	 */
1149 4
	public function setStateCondition($state)
1150
	{
1151
		switch ($state) {
1152
			default:
1153
			case 'Active':
1154
				$this->stateCondition = 0;
1155
				break;
1156
			case 'Trash':
1157
				$this->stateCondition = 1;
1158
				break;
1159
			case 'Archived':
1160
				$this->stateCondition = 2;
1161
				break;
1162
			case 'All':
1163
				$this->stateCondition = null;
1164
				break;
1165
		}
1166
		return $this;
1167
	}
1168
1169
	/**
1170
	 * Returns condition for field in this module.
1171
	 *
1172
	 * @param string $fieldName
1173
	 * @param mixed  $value
1174
	 * @param string $operator
1175
	 * @param bool   $userFormat
1176
	 *
1177
	 * @throws \App\Exceptions\AppException
1178
	 *
1179
	 * @return array|bool
1180
	 */
1181
	private function getCondition(string $fieldName, $value, string $operator, bool $userFormat = false)
1182
	{
1183
		$queryField = $this->getQueryField($fieldName);
1184
		if ($userFormat && $queryField->getField()) {
1185
			$value = $queryField->getField()->getUITypeModel()->getDbConditionBuilderValue($value, $operator);
1186
		}
1187
		$queryField->setValue($value);
1188
		$queryField->setOperator($operator);
1189
		$condition = $queryField->getCondition();
1190
		if ($condition && ($field = $this->getModuleField($fieldName)) && !isset($this->tablesList[$field->getTableName()])) {
1191
			$this->tablesList[$field->getTableName()] = $field->getTableName();
1192
		}
1193
		return $condition;
1194
	}
1195
1196
	/**
1197
	 * Gets conditions for inventory field.
1198
	 *
1199
	 * @param string $fieldName
1200
	 * @param mixed  $value
1201
	 * @param string $operator
1202
	 * @param bool   $userFormat
1203
	 *
1204
	 * @return array
1205
	 */
1206
	private function getInvCondition(string $fieldName, $value, string $operator, bool $userFormat = false): array
1207
	{
1208
		$parseData = [];
1209
		if ($this->getModuleModel()->isInventory()) {
1210
			$queryField = $this->getQueryInvField($fieldName);
1211
			if ($userFormat) {
1212
				$value = $queryField->getDbConditionBuilderValue($value, $operator);
0 ignored issues
show
Bug introduced by
The method getDbConditionBuilderValue() does not exist on App\Conditions\QueryFields\Inventory\BaseField. ( Ignorable by Annotation )

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

1212
				/** @scrutinizer ignore-call */ 
1213
    $value = $queryField->getDbConditionBuilderValue($value, $operator);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1213
			}
1214
			$queryField->setValue($value);
1215
			$queryField->setOperator($operator);
1216
			$parseData = $queryField->getCondition();
1217
			$invTableName = $this->getModuleModel()->getInventoryModel()->getDataTableName();
1218
			$tableName = $this->getModuleModel()->getBaseTableName();
1219
			$tableIndex = $this->getModuleModel()->getBaseTableIndex();
1220
			$this->addJoin(['LEFT JOIN', $invTableName, "{$tableName}.{$tableIndex} = {$invTableName}.crmid"]);
1221
			if (null === $this->distinct) {
1222
				$this->setDistinct('id');
1223
			}
1224
		}
1225
1226
		return $parseData;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $parseData could return the type boolean which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1227
	}
1228
1229
	/**
1230
	 * Returns condition for field in related module.
1231
	 *
1232
	 * @param array $condition
1233
	 *
1234
	 * @throws \App\Exceptions\AppException
1235
	 *
1236
	 * @return array|bool
1237
	 */
1238
	private function getRelatedCondition(array $condition)
1239
	{
1240
		$field = $this->addRelatedJoin($condition);
1241
		if (!$field) {
1242
			Log::error('Not found source field', __METHOD__);
1243
			return false;
1244
		}
1245
		$queryField = $this->getQueryRelatedField($condition, $field);
1246
		$queryField->setValue($condition['value']);
1247 4
		$queryField->setOperator($condition['operator']);
1248
		return $queryField->getCondition();
1249 4
	}
1250
1251
	/**
1252 4
	 * Set condition.
1253
	 *
1254
	 * @param string $fieldName
1255
	 * @param mixed  $value
1256
	 * @param string $operator
1257 4
	 * @param mixed  $groupAnd
1258
	 * @param bool   $userFormat
1259 4
	 *
1260
	 * @see Condition::ADVANCED_FILTER_OPTIONS
1261
	 * @see Condition::DATE_OPERATORS
1262 4
	 *
1263
	 * @return $this
1264
	 */
1265
	public function addCondition($fieldName, $value, $operator, $groupAnd = true, $userFormat = false)
1266
	{
1267
		$condition = $this->getCondition($fieldName, $value, $operator, $userFormat);
1268
		if ($condition) {
1269
			if ($groupAnd) {
1270
				$this->conditionsAnd[] = $condition;
1271 2
			} else {
1272
				$this->conditionsOr[] = $condition;
1273 2
			}
1274
		} else {
1275
			Log::error('Wrong condition');
1276 2
		}
1277 2
		return $this;
1278 2
	}
1279 2
1280 2
	/**
1281
	 * Get query field instance.
1282
	 *
1283 2
	 * @param string $fieldName
1284 2
	 *
1285 2
	 * @throws \App\Exceptions\AppException
1286 2
	 *
1287 2
	 * @return \App\Conditions\QueryFields\BaseField
1288 2
	 */
1289 2
	public function getQueryField($fieldName)
1290
	{
1291
		if (isset($this->queryFields[$fieldName])) {
1292 2
			return $this->queryFields[$fieldName];
1293
		}
1294 2
		if ('id' === $fieldName) {
1295
			$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

1295
			$queryField = new Conditions\QueryFields\IdField($this, /** @scrutinizer ignore-type */ '');
Loading history...
1296
			return $this->queryFields[$fieldName] = $queryField;
1297 2
		}
1298
		$field = $this->getModuleField($fieldName);
1299
		if (empty($field)) {
1300
			Log::error("Not found field model | Field name: '$fieldName' in module" . $this->getModule());
1301
			throw new \App\Exceptions\AppException("ERR_NOT_FOUND_FIELD_MODEL|$fieldName|" . $this->getModule());
1302
		}
1303
		$className = '\App\Conditions\QueryFields\\' . ucfirst($field->getFieldDataType()) . 'Field';
1304
		if (!class_exists($className)) {
1305
			Log::error('Not found query field condition | FieldDataType: ' . ucfirst($field->getFieldDataType()));
1306
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_QUERY_FIELD_CONDITION|' . $fieldName);
1307
		}
1308
		$queryField = new $className($this, $field);
1309
		return $this->queryFields[$fieldName] = $queryField;
1310
	}
1311
1312
	/**
1313
	 * Gets query field instance for inventory field.
1314
	 *
1315
	 * @param string $fieldName
1316 2
	 *
1317
	 * @return Conditions\QueryFields\Inventory\BaseField
1318
	 */
1319 2
	public function getQueryInvField(string $fieldName): Conditions\QueryFields\Inventory\BaseField
1320 2
	{
1321
		$field = $this->getModuleModel()->getInventoryModel()->getField($fieldName);
1322 2
		if (empty($field)) {
1323
			Log::error("Not found inv field model | Field name: '{$fieldName}' in module" . $this->getModule());
1324
			throw new \App\Exceptions\AppException("ERR_NOT_FOUND_FIELD_MODEL|$fieldName|" . $this->getModule());
1325
		}
1326
1327
		$className = '\App\Conditions\QueryFields\Inventory\\' . ucfirst($field->getType()) . 'Field';
1328
		if (!class_exists($className)) {
1329
			Log::error('Not found query inv field condition | FieldDataType: ' . ucfirst($field->getType()));
1330
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_QUERY_INV_FIELD_CONDITION|' . $fieldName);
1331
		}
1332
1333
		return new $className($this, $field);
1334
	}
1335
1336
	/**
1337
	 * Set condition on reference module fields.
1338
	 *
1339
	 * @param array $condition
1340
	 */
1341
	public function addRelatedCondition($condition)
1342
	{
1343
		$queryCondition = $this->getRelatedCondition($condition);
1344
		if ($queryCondition) {
1345
			if ($condition['conditionGroup']) {
1346
				$this->conditionsAnd[] = $queryCondition;
1347
			} else {
1348
				$this->conditionsOr[] = $queryCondition;
1349
			}
1350
		} else {
1351
			Log::error('Wrong condition');
1352
		}
1353
	}
1354
1355
	/**
1356
	 * Set related field join.
1357
	 *
1358
	 * @param string[] $fieldDetail
1359
	 *
1360
	 * @return bool|\Vtiger_Field_Model
1361
	 */
1362
	public function addRelatedJoin($fieldDetail)
1363
	{
1364
		$relatedFieldModel = $this->getRelatedModuleField($fieldDetail['relatedField'], $fieldDetail['relatedModule']);
1365
		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...
1366
			Log::warning("Field in related module is inactive or does not exist. Related module: {$fieldDetail['relatedModule']} | Related field: {$fieldDetail['relatedField']}");
1367
			return false;
1368
		}
1369
		$tableName = $relatedFieldModel->getTableName();
1370
		$sourceFieldModel = $this->getModuleField($fieldDetail['sourceField']);
1371
		$relatedTableName = $tableName . $fieldDetail['sourceField'];
1372
		$relatedTableIndex = $relatedFieldModel->getModule()->getEntityInstance()->tab_name_index[$tableName];
1373
		$this->addJoin(['LEFT JOIN', "$tableName $relatedTableName", "{$sourceFieldModel->getTableName()}.{$sourceFieldModel->getColumnName()} = $relatedTableName.$relatedTableIndex"]);
1374
		return $relatedFieldModel;
1375
	}
1376
1377
	/**
1378
	 * Get query related field instance.
1379
	 *
1380
	 * @param array|string        $relatedInfo
1381
	 * @param \Vtiger_Field_Model $field
1382
	 *
1383
	 * @throws \App\Exceptions\AppException
1384
	 *
1385
	 * @return \App\Conditions\QueryFields\BaseField
1386
	 */
1387
	public function getQueryRelatedField($relatedInfo, ?\Vtiger_Field_Model $field = null)
1388
	{
1389
		if (!\is_array($relatedInfo)) {
1390
			[$fieldName, $relatedModule, $sourceFieldName] = array_pad(explode(':', $relatedInfo), 3, false);
1391
			$relatedInfo = [
1392
				'sourceField' => $sourceFieldName,
1393
				'relatedModule' => $relatedModule,
1394
				'relatedField' => $fieldName,
1395
			];
1396
		}
1397
		$relatedModule = $relatedInfo['relatedModule'];
1398
		$fieldName = $relatedInfo['relatedField'];
1399
1400
		if (isset($this->relatedQueryFields[$relatedModule][$fieldName])) {
1401
			$queryField = clone $this->relatedQueryFields[$relatedModule][$fieldName];
1402
			$queryField->setRelated($relatedInfo);
1403
			return $queryField;
1404
		}
1405
		if (null === $field) {
1406
			$field = $this->getRelatedModuleField($fieldName, $relatedModule);
1407
		}
1408
		$className = '\App\Conditions\QueryFields\\' . ucfirst($field->getFieldDataType()) . 'Field';
1409
		if (!class_exists($className)) {
1410
			Log::error('Not found query field condition');
1411
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_QUERY_FIELD_CONDITION');
1412
		}
1413
		$queryField = new $className($this, $field);
1414
		$queryField->setRelated($relatedInfo);
1415
		return $this->relatedQueryFields[$relatedModule][$field->getName()] = $queryField;
1416
	}
1417
1418
	/**
1419
	 * Set order for related module.
1420
	 *
1421
	 * @param string[] $orderDetail
1422
	 */
1423
	public function setRelatedOrder(array $orderDetail)
1424
	{
1425
		$field = $this->addRelatedJoin($orderDetail);
1426
		if (!$field) {
1427
			Log::error('Not found source field');
1428
		}
1429
		$queryField = $this->getQueryRelatedField($orderDetail, $field);
1430
		$this->order = array_merge($this->order, $queryField->getOrderBy($orderDetail['relatedSortOrder']));
1431
	}
1432
1433
	/**
1434
	 * Sets the ORDER BY part of the query.
1435
	 */
1436
	public function loadOrder()
1437
	{
1438
		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...
1439
			$this->query->orderBy($this->order);
1440
		}
1441
	}
1442
1443
	/**
1444
	 * Sets the GROUP BY part of the query.
1445
	 */
1446
	public function loadGroup()
1447
	{
1448
		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...
1449
			$this->query->groupBy(array_unique($this->group));
1450
		}
1451
	}
1452
1453
	/**
1454
	 * Parse base search condition to db condition.
1455
	 *
1456
	 * @param array $searchParams Example: [[["firstname","a","Tom"]]]
1457
	 *
1458
	 * @return array
1459
	 */
1460
	public function parseBaseSearchParamsToCondition($searchParams)
1461
	{
1462
		if (empty($searchParams)) {
1463
			return [];
1464
		}
1465
		$advFilterConditionFormat = [];
1466
		$glueOrder = ['and', 'or'];
1467
		$groupIterator = 0;
1468
		foreach ($searchParams as $groupInfo) {
1469
			if (!empty($groupInfo)) {
1470
				$groupColumnsInfo = [];
1471
				foreach ($groupInfo as $fieldSearchInfo) {
1472
					if ($fieldSearchInfo) {
1473
						[$fieldNameInfo, $operator, $fieldValue] = array_pad($fieldSearchInfo, 3, false);
1474
						$fieldValue = Purifier::decodeHtml($fieldValue);
1475
						[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $fieldNameInfo), 3, false);
1476
						if (!empty($sourceFieldName)) {
1477
							$field = $this->getRelatedModuleField($fieldName, $moduleName);
1478
						} else {
1479
							$field = $this->getModuleField($fieldName);
1480
						}
1481
						if ($field && ('date_start' === $fieldName || 'due_date' === $fieldName || 'datetime' === $field->getFieldDataType())) {
1482
							$dateValues = explode(',', $fieldValue);
1483
							//Indicate whether it is fist date in the between condition
1484
							$isFirstDate = true;
1485
							foreach ($dateValues as $key => $dateValue) {
1486
								$dateTimeCompoenents = explode(' ', $dateValue);
1487
								if (empty($dateTimeCompoenents[1])) {
1488
									if ($isFirstDate) {
1489
										$dateTimeCompoenents[1] = '00:00:00';
1490
									} else {
1491
										$dateTimeCompoenents[1] = '23:59:59';
1492
									}
1493
								}
1494
								$dateValue = implode(' ', $dateTimeCompoenents);
1495
								$dateValues[$key] = $dateValue;
1496
								$isFirstDate = false;
1497
							}
1498
							$fieldValue = implode(',', $dateValues);
1499
						}
1500
						$groupColumnsInfo[] = ['field_name' => $fieldName, 'module_name' => $moduleName, 'source_field_name' => $sourceFieldName, 'comparator' => $operator, 'value' => $fieldValue];
1501
					}
1502
				}
1503
				$advFilterConditionFormat[$glueOrder[$groupIterator]] = $groupColumnsInfo;
1504
			}
1505
			++$groupIterator;
1506
		}
1507
		return $advFilterConditionFormat;
1508
	}
1509
1510
	/**
1511
	 * Parse search condition to standard condition.
1512
	 *
1513
	 * @param array $searchParams
1514
	 *
1515
	 * @return array
1516
	 */
1517
	public function parseSearchParams(array $searchParams): array
1518
	{
1519
		$glueOrder = ['AND', 'OR'];
1520
		$searchParamsConditions = [];
1521
		foreach ($searchParams as $key => $conditions) {
1522
			if (empty($conditions)) {
1523
				continue;
1524
			}
1525
			$searchParamsConditions['condition'] = $glueOrder[$key];
1526
			$searchParamsConditions['rules'] = [];
1527
			foreach ($conditions as $condition) {
1528
				[$fieldName, , $sourceFieldName] = array_pad(explode(':', $condition[0]), 3, false);
1529
				if (!$sourceFieldName) {
1530
					$condition[0] = "{$fieldName}:{$this->getModule()}";
1531
				}
1532
				$searchParamsConditions['rules'][] = ['fieldname' => $condition[0], 'operator' => $condition[1], 'value' => $condition[2]];
1533
			}
1534
		}
1535
		return $searchParamsConditions;
1536
	}
1537
}
1538