AutoAssign   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 60
eloc 166
c 0
b 0
f 0
dl 0
loc 405
rs 3.6

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstancesByModule() 0 7 2
A getByModule() 0 13 3
B getAvailableUsersQuery() 0 35 7
A getAutoAssignForRecord() 0 14 5
A getMembers() 0 19 4
A getQueryByRoundRobin() 0 10 1
A getModuleName() 0 3 1
A getId() 0 3 1
A isActive() 0 17 5
A getQueryByLoadBalance() 0 3 1
A checkConditionForRecord() 0 4 2
A getDefaultOwner() 0 14 6
A getInstance() 0 3 2
A getQuery() 0 28 3
A postProcess() 0 11 5
A getName() 0 3 1
A getOwners() 0 15 3
A getOwner() 0 15 6
A getInstanceById() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like AutoAssign often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AutoAssign, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Auto Assign file.
4
 *
5
 * The file is part of the paid functionality. Using the file is allowed only after purchasing a subscription.
6
 * File modification allowed only with the consent of the system producer.
7
 *
8
 * @package App
9
 *
10
 * @copyright YetiForce S.A.
11
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
12
 * @author    Radosław Skrzypczak <[email protected]>
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 */
15
16
namespace App;
17
18
/**
19
 * Auto Assign class.
20
 */
21
class AutoAssign extends Base
22
{
23
	/** @var string Basic table name */
24
	public const TABLE_NAME = 's_#__auto_assign';
25
	/** @var array Members tables */
26
	public const MEMBERS_TABLES = ['s_#__auto_assign_users' => 'id', 's_#__auto_assign_groups' => 'id', 's_#__auto_assign_roles' => 'id'];
27
	/** @var string Round robin table name */
28
	public const ROUND_ROBIN_TABLE = 'u_#__auto_assign_rr';
29
30
	/** @var int Status inactive */
31
	public const STATUS_INACTIVE = 0;
32
	/** @var int Status active */
33
	public const STATUS_ACTIVE = 1;
34
35
	/** @var int Manual mode */
36
	public const MODE_MANUAL = 1;
37
	/** @var int Handler mode */
38
	public const MODE_HANDLER = 2;
39
	/** @var int Workflow mode */
40
	public const MODE_WORKFLOW = 4;
41
42
	/** @var int Load balance method */
43
	public const METHOD_LOAD_BALANCE = 0;
44
	/** @var int Round robin method */
45
	public const METHOD_ROUND_ROBIN = 1;
46
47
	/**
48
	 * Get all auto assign entries for module.
49
	 *
50
	 * @param string $moduleName
51
	 * @param int    $mode       A bitmask of one or more of the mode flags
52
	 * @param int    $state
53
	 *
54
	 * @return array
55
	 */
56
	public static function getByModule(string $moduleName, int $mode = self::MODE_HANDLER | self::MODE_WORKFLOW | self::MODE_MANUAL, int $state = self::STATUS_ACTIVE): array
57
	{
58
		$query = (new Db\Query())->from(self::TABLE_NAME)
59
			->where(['tabid' => Module::getModuleId($moduleName), 'state' => $state]);
60
		$mods = ['or'];
61
		foreach ([self::MODE_MANUAL => 'gui', self::MODE_HANDLER => 'handler', self::MODE_WORKFLOW => 'workflow'] as $key => $column) {
62
			if ($mode & $key) {
63
				$mods[] = [$column => 1];
64
			}
65
		}
66
		$query->andWhere($mods);
67
68
		return $query->all();
69
	}
70
71
	/**
72
	 * Get all auto assign instances for module.
73
	 *
74
	 * @param string   $moduleName
75
	 * @param int|null $mode       A bitmask of one or more of the mode flags
76
	 *
77
	 * @return array
78
	 */
79
	public static function getInstancesByModule(string $moduleName, int $mode = null): array
80
	{
81
		$instances = [];
82
		foreach (self::getByModule($moduleName, $mode) as $autoAssignData) {
83
			$instances[$autoAssignData['id']] = self::getInstance($autoAssignData);
84
		}
85
		return $instances;
86
	}
87
88
	/**
89
	 * Get auto assign instance for record.
90
	 *
91
	 * @param \Vtiger_Record_Model $recordModel
92
	 * @param int|null             $mode        A bitmask of one or more of the mode flags
93
	 *
94
	 * @return self|null
95
	 */
96
	public static function getAutoAssignForRecord(\Vtiger_Record_Model $recordModel, int $mode = null): ?self
97
	{
98
		$autoAssignInstance = null;
99
		if (!\App\YetiForce\Shop::check('YetiForceAutoAssignment')) {
100
			return $autoAssignInstance;
101
		}
102
		foreach (self::getByModule($recordModel->getModuleName(), $mode) as $autoAssignData) {
103
			$conditions = Json::isEmpty($autoAssignData['conditions']) ? [] : Json::decode($autoAssignData['conditions']);
104
			if (Condition::checkConditions($conditions, $recordModel)) {
105
				$autoAssignInstance = self::getInstance($autoAssignData);
106
				break;
107
			}
108
		}
109
		return $autoAssignInstance;
110
	}
111
112
	/**
113
	 * Get auto assign instance by ID.
114
	 *
115
	 * @param int $id
116
	 *
117
	 * @return self|null
118
	 */
119
	public static function getInstanceById(int $id): ?self
120
	{
121
		$data = (new Db\Query())->from(self::TABLE_NAME)->where(['id' => $id])->one();
122
		return $data ? (new self())->setData($data) : null;
123
	}
124
125
	/**
126
	 * Get auto assign instance by data.
127
	 *
128
	 * @param array $data
129
	 *
130
	 * @return self|null
131
	 */
132
	public static function getInstance(array $data): ?self
133
	{
134
		return $data ? (new self())->setData($data) : null;
135
	}
136
137
	/**
138
	 * Function to get the Id.
139
	 *
140
	 * @return int
141
	 */
142
	public function getId(): int
143
	{
144
		return $this->get('id');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('id') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
145
	}
146
147
	/**
148
	 * Get name of auto assign instance.
149
	 *
150
	 * @param bool $encode
151
	 *
152
	 * @return string
153
	 */
154
	public function getName(bool $encode = true): string
155
	{
156
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', false, $encode);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type null|string expected by parameter $language of App\Language::translate(). ( Ignorable by Annotation )

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

156
		return Language::translate($this->get('subject'), 'Settings:AutomaticAssignment', /** @scrutinizer ignore-type */ false, $encode);
Loading history...
157
	}
158
159
	/**
160
	 * Get module name.
161
	 *
162
	 * @return string
163
	 */
164
	public function getModuleName(): string
165
	{
166
		return Module::getModuleName($this->get('tabid'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return App\Module::getMo...me($this->get('tabid')) could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
167
	}
168
169
	/**
170
	 * Check conditions for record.
171
	 *
172
	 * @param \Vtiger_Record_Model $recordModel
173
	 *
174
	 * @return bool
175
	 */
176
	public function checkConditionForRecord(\Vtiger_Record_Model $recordModel): bool
177
	{
178
		$conditions = Json::isEmpty($this->get('conditions')) ? [] : Json::decode($this->get('conditions'));
179
		return Condition::checkConditions($conditions, $recordModel);
180
	}
181
182
	/**
183
	 * Check if the instance is active in a given mode.
184
	 *
185
	 * @param int $mode
186
	 *
187
	 * @return bool
188
	 */
189
	public function isActive(int $mode): bool
190
	{
191
		switch ($mode) {
192
			case self::MODE_MANUAL:
193
				$result = !$this->isEmpty('gui');
194
				break;
195
			case self::MODE_HANDLER:
196
				$result = !$this->isEmpty('handler');
197
				break;
198
			case self::MODE_WORKFLOW:
199
				$result = !$this->isEmpty('workflow');
200
				break;
201
			default:
202
				$result = false;
203
				break;
204
		}
205
		return $result && self::STATUS_ACTIVE === (int) $this->get('state');
206
	}
207
208
	/**
209
	 * Get an automatic selected user ID.
210
	 *
211
	 * @return int
212
	 */
213
	public function getOwner(): ?int
214
	{
215
		switch ($this->get('method')) {
216
			case self::METHOD_LOAD_BALANCE:
217
				$owner = $this->getQueryByLoadBalance()->scalar() ?: null;
218
				break;
219
			case self::METHOD_ROUND_ROBIN:
220
				$owner = $this->getQueryByRoundRobin()->scalar() ?: null;
221
				break;
222
			default:
223
				$owner = null;
224
				break;
225
		}
226
227
		return $owner ? $owner : $this->getDefaultOwner();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $owner ? $owner : $this->getDefaultOwner() could return the type string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
228
	}
229
230
	/**
231
	 * Get automatic selected users.
232
	 *
233
	 * @return array
234
	 */
235
	public function getOwners(): array
236
	{
237
		switch ($this->get('method')) {
238
			case self::METHOD_LOAD_BALANCE:
239
				$owner = $this->getQueryByLoadBalance()->all();
240
				break;
241
			case self::METHOD_ROUND_ROBIN:
242
				$owner = $this->getQueryByRoundRobin()->all();
243
				break;
244
			default:
245
				$owner = [];
246
				break;
247
		}
248
249
		return $owner;
250
	}
251
252
	/**
253
	 * Get default owner.
254
	 *
255
	 * @return int
256
	 */
257
	public function getDefaultOwner(): ?int
258
	{
259
		$owner = null;
260
		$defaultOwner = (int) $this->get('default_assign');
261
		$ownerModel = Fields\Owner::getInstance($this->getModuleName());
262
263
		$type = $defaultOwner ? Fields\Owner::getType($defaultOwner) : null;
264
		if ('Users' === $type) {
0 ignored issues
show
introduced by
The condition 'Users' === $type is always false.
Loading history...
265
			$owner = User::isExists($defaultOwner) ? $defaultOwner : $owner;
266
		} elseif ($type) {
267
			$owner = \array_key_exists($defaultOwner, $ownerModel->getAccessibleGroupForModule()) ? $defaultOwner : $owner;
268
		}
269
270
		return $owner;
271
	}
272
273
	/**
274
	 * Query object for users allowed to be assigned by load balanced method.
275
	 *
276
	 * In order to correctly balance the entries attribution
277
	 * we need ot randomize the order in which they are returned.
278
	 * Otherwise, when multiple users have the same amount of entries
279
	 * it is always the first one in the results who will be assigned to new entry.
280
	 *
281
	 * @return Db\Query
282
	 */
283
	public function getQueryByLoadBalance(): Db\Query
284
	{
285
		return $this->getQuery()->orderBy(['count' => SORT_ASC, new \yii\db\Expression('RAND()')]);
286
	}
287
288
	/**
289
	 * Query object for users allowed to be assigned by round robin.
290
	 *
291
	 * @return Db\Query
292
	 */
293
	public function getQueryByRoundRobin(): Db\Query
294
	{
295
		$robinTable = self::ROUND_ROBIN_TABLE;
296
		$columnName = "{$robinTable}.datetime";
297
		$id = $this->getId();
298
299
		return $this->getQuery()->leftJoin($robinTable, "vtiger_users.id = {$robinTable}.user AND {$robinTable}.id={$id}")
300
			->addSelect([$columnName])
301
			->addGroupBy($columnName)
302
			->orderBy([$columnName => SORT_ASC]);
303
	}
304
305
	/**
306
	 * Query object for users allowed for assignment.
307
	 *
308
	 * @return Db\Query
309
	 */
310
	public function getQuery(): Db\Query
311
	{
312
		$ownerFieldName = 'assigned_user_id';
313
		$queryGeneratorUsers = $this->getAvailableUsersQuery();
314
315
		$queryGenerator = (new QueryGenerator($this->getModuleName()));
316
		$queryGenerator->permissions = false;
317
		$conditions = Json::isEmpty($this->get('record_limit_conditions')) ? [] : Json::decode($this->get('record_limit_conditions'));
318
		$queryGenerator->setFields([$ownerFieldName])
319
			->setCustomColumn(['count' => new \yii\db\Expression('COUNT(*)')])
320
			->setConditions($conditions)
321
			->setGroup($ownerFieldName)
322
			->addNativeCondition([$queryGenerator->getColumnName($ownerFieldName) => $queryGeneratorUsers->createQuery()]);
323
		$subQuery = $queryGenerator->createQuery();
324
325
		$recordLimit = (int) $this->get('record_limit');
326
		if (0 === $recordLimit) {
327
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => $queryGeneratorUsers->getColumnName('records_limit')]);
328
		} else {
329
			$queryGeneratorUsers->setCustomColumn(['temp_limit' => new \yii\db\Expression($recordLimit)]);
330
		}
331
		$queryGeneratorUsers->setGroup('id')->setCustomGroup(['temp_limit', 'count']);
332
		$query = $queryGeneratorUsers->createQuery(true);
333
		$query->leftJoin(['crm_data_temp_table' => $subQuery], "crm_data_temp_table.{$ownerFieldName}={$queryGeneratorUsers->getColumnName('id')}");
334
		$query->addSelect(['crm_data_temp_table.count']);
335
		$query->andHaving(['or', ['<', 'count', new \yii\db\Expression('temp_limit')], ['temp_limit' => 0], ['count' => null]]);
336
337
		return $query;
338
	}
339
340
	/**
341
	 * Query generator object of available users.
342
	 *
343
	 * @return QueryGenerator
344
	 */
345
	public function getAvailableUsersQuery(): QueryGenerator
346
	{
347
		$queryGenerator = (new QueryGenerator('Users'))
348
			->setFields(['id'])
349
			->addCondition('status', 'Active', 'e')
350
			->addCondition('available', 1, 'e')
351
			->addCondition('auto_assign', 1, 'e');
352
		$columnName = $queryGenerator->getColumnName('id');
353
354
		$condition = ['or'];
355
		foreach ($this->getMembers() as $member) {
356
			[$type, $id] = explode(':', $member);
357
			switch ($type) {
358
				case PrivilegeUtil::MEMBER_TYPE_USERS:
359
					$condition[$type][$columnName][] = (int) $id;
360
					break;
361
				case PrivilegeUtil::MEMBER_TYPE_GROUPS:
362
					$condition[] = [$columnName => (new Db\Query())->select(['userid'])->from(["condition_{$type}_{$id}_" . Layout::getUniqueId() => PrivilegeUtil::getQueryToUsersByGroup((int) $id)])];
363
					break;
364
				case PrivilegeUtil::MEMBER_TYPE_ROLES:
365
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRole($id)];
366
					break;
367
				case PrivilegeUtil::MEMBER_TYPE_ROLE_AND_SUBORDINATES:
368
					$condition[] = [$columnName => PrivilegeUtil::getQueryToUsersByRoleAndSubordinate($id)];
369
					break;
370
				default:
371
					break;
372
			}
373
		}
374
		if (1 === \count($condition)) {
375
			$condition = [$columnName => 0];
376
		}
377
		$queryGenerator->addNativeCondition($condition);
378
379
		return $queryGenerator;
380
	}
381
382
	/**
383
	 * Get members.
384
	 *
385
	 * @return array
386
	 */
387
	public function getMembers(): array
388
	{
389
		if (!$this->has('members')) {
390
			$queryAll = null;
391
			foreach (self::MEMBERS_TABLES as $tableName => $index) {
392
				$query = (new Db\Query())
393
					->select(['member' => new \yii\db\Expression('CONCAT(type,\':\', member)')])
394
					->from($tableName)
395
					->where(["{$tableName}.{$index}" => $this->getId()]);
396
				if ($queryAll) {
397
					$queryAll->union($query, true);
398
				} else {
399
					$queryAll = $query;
400
				}
401
			}
402
			$members = $queryAll->column();
0 ignored issues
show
Bug introduced by
The method column() does not exist on null. ( Ignorable by Annotation )

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

402
			/** @scrutinizer ignore-call */ 
403
   $members = $queryAll->column();

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...
403
			$this->set('members', $members);
404
		}
405
		return $this->get('members');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('members') could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
406
	}
407
408
	/**
409
	 * Post process action.
410
	 *
411
	 * @param int $userId
412
	 *
413
	 * @return void
414
	 */
415
	public function postProcess(int $userId)
416
	{
417
		$dbCommand = Db::getInstance()->createCommand();
418
		if ($userId && self::METHOD_ROUND_ROBIN === (int) $this->get('method')) {
419
			$params = ['id' => $this->getId(), 'user' => $userId];
420
			$isExists = (new Db\Query())->from(self::ROUND_ROBIN_TABLE)->where($params)->exists();
421
			if ($isExists) {
422
				$dbCommand->update(self::ROUND_ROBIN_TABLE, ['datetime' => (new \DateTime())->format('Y-m-d H:i:s.u')], $params)->execute();
423
			} elseif (\App\User::isExists($userId, false)) {
424
				$params['datetime'] = (new \DateTime())->format('Y-m-d H:i:s.u');
425
				$dbCommand->insert(self::ROUND_ROBIN_TABLE, $params)->execute();
426
			}
427
		}
428
	}
429
}
430