Passed
Push — developer ( 2a4a55...bf97c6 )
by Radosław
18:45
created

RecordNumber::getRelatedValue()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 13.125

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
ccs 3
cts 6
cp 0.5
rs 8.8333
c 0
b 0
f 0
cc 7
nc 3
nop 1
crap 13.125
1
<?php
2
3
namespace App\Fields;
4
5
/**
6
 * Record number class.
7
 *
8
 * @package App
9
 *
10
 * @copyright YetiForce S.A.
11
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
12
 * @author    Tomasz Kur <[email protected]>
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 * @author    Radosław Skrzypczak <[email protected]>
15
 */
16
class RecordNumber extends \App\Base
17
{
18
	/**
19
	 * Instance cache by module id.
20
	 *
21
	 * @var \App\Fields\RecordNumber[]
22 33
	 */
23
	private static array $instanceCache = [];
24 33
	/**
25 33
	 * Sequence number field cache by module id.
26 30
	 *
27
	 * @var array
28 33
	 */
29 33
	private static array $sequenceNumberFieldCache = [];
30 33
31 33
	/**
32
	 * Related value.
33
	 *
34
	 * @var string
35
	 */
36
	private $relatedValue;
37
38
	/**
39 24
	 * Function to get instance.
40
	 *
41 24
	 * @param int|string $tabId
42 24
	 *
43
	 * @return \App\Fields\RecordNumber
44
	 */
45
	public static function getInstance($tabId): self
46
	{
47
		if (!is_numeric($tabId)) {
48
			$tabId = \App\Module::getModuleId($tabId);
49 24
		}
50
		if (isset(static::$instanceCache[$tabId])) {
0 ignored issues
show
Bug introduced by
Since $instanceCache is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $instanceCache to at least protected.
Loading history...
51 24
			return static::$instanceCache[$tabId];
52
		}
53
		$instance = new static();
54
		$row = (new \App\Db\Query())->from('vtiger_modentity_num')->where(['tabid' => $tabId])->one() ?: [];
55
		$row['tabid'] = $tabId;
56
		$instance->setData($row);
57
58
		return static::$instanceCache[$tabId] = $instance;
59 16
	}
60
61 16
	/**
62 16
	 * Get data for update.
63 4
	 *
64
	 * @return $this
65 16
	 */
66
	public function getNumDataForUpdate()
67
	{
68 16
		$sql = (new \App\Db\Query())->from('vtiger_modentity_num')->where(['tabid' => $this->get('tabid')])->createCommand()->getRawSql() . ' FOR UPDATE ';
69 16
		$row = \App\Db::getInstance()->createCommand($sql)->queryOne();
70 16
		$row['tabid'] = $this->get('tabid');
71 4
72
		return $this->setData(array_merge($this->getData(), $row));
73 16
	}
74 16
75
	/**
76 16
	 * Sets model of record.
77 16
	 *
78
	 * @param \Vtiger_Record_Model $recordModel
79
	 *
80
	 * @return $this
81
	 */
82
	public function setRecord(\Vtiger_Record_Model $recordModel): self
83
	{
84
		$this->set('recordModel', $recordModel);
85 16
		return $this;
86
	}
87 16
88 16
	/**
89
	 * Returns model of record.
90
	 *
91
	 * @return \Vtiger_Record_Model|null
92
	 */
93
	public function getRecord(): ?\Vtiger_Record_Model
94
	{
95
		return $this->get('recordModel');
96
	}
97
98 29
	/**
99
	 * Function to get the next nuber of record.
100 29
	 *
101 29
	 * @return string
102
	 */
103
	public function getIncrementNumber(): string
104
	{
105
		$value = $this->getRelatedValue();
106
		$this->getNumDataForUpdate();
107
		$actualSequence = static::getSequenceNumber($this->get('reset_sequence'));
108
		if ($this->get('reset_sequence') && $this->get('cur_sequence') !== $actualSequence) {
109
			$currentSequenceNumber = 1;
110
			$this->updateModuleVariablesSequences($currentSequenceNumber);
111 17
		} else {
112
			$currentSequenceNumber = $this->get('cur_id');
113
			if ($value) {
114
				$sql = (new \App\Db\Query())->select(['cur_id'])->from('u_#__modentity_sequences')->where(['tabid' => $this->get('tabid'), 'value' => $value])->createCommand()->getRawSql() . ' FOR UPDATE ';
115 17
				$currentSequenceNumber = \App\Db::getInstance()->createCommand($sql)->queryScalar() ?: 1;
116
			}
117
		}
118
119
		$reqNo = $currentSequenceNumber + 1;
120
		$this->setNumberSequence($reqNo, $actualSequence);
121
		$fullPrefix = $this->parseNumber($currentSequenceNumber);
122
123
		return \App\Purifier::decodeHtml($fullPrefix);
124
	}
125
126 16
	/**
127
	 * Gets related value.
128 16
	 *
129 16
	 * @param bool $reload
130 16
	 *
131 16
	 * @return string
132 16
	 */
133
	public function getRelatedValue(bool $reload = false): string
134
	{
135
		if (!isset($this->relatedValue) || $reload) {
136
			$value = [];
137
			preg_match_all('/{{picklist:([a-zA-Z0-9_]+)}}|\$\((\w+) : ([,"\+\#\%\.\=\-\[\]\&\w\s\|\)\(\:]+)\)\$/u', $this->get('prefix') . $this->get('postfix'), $matches);
138
			if ($this->getRecord() && !empty($matches[0])) {
139
				foreach ($matches[0] as $key => $element) {
140 16
					if (0 === strpos($element, '{{picklist:')) {
141 16
						$value[] = $this->getPicklistValue($matches[1][$key]);
142
					} else {
143
						$value[] = \App\TextParser::getInstanceByModel($this->getRecord())->setGlobalPermissions(false)->setContent($element)->parse()->getContent();
144
					}
145
				}
146
			}
147
			$this->relatedValue = implode('|', $value);
148 24
		}
149
		return $this->relatedValue;
150 24
	}
151 24
152
	/**
153
	 * Parse number based on postfix and prefix.
154
	 *
155
	 * @param int $seq
156
	 *
157
	 * @return string
158
	 */
159
	public function parseNumber(int $seq): string
160
	{
161
		$string = str_replace(['{{YYYY}}', '{{YY}}', '{{MM}}', '{{M}}', '{{DD}}', '{{D}}'], [static::date('Y'), static::date('y'), static::date('m'), static::date('n'), static::date('d'), static::date('j')], $this->get('prefix') . str_pad((string) $seq, $this->get('leading_zeros'), '0', STR_PAD_LEFT) . $this->get('postfix'));
162
		if ($this->getRecord()) {
163
			$string = \App\TextParser::getInstanceByModel($this->getRecord())->setGlobalPermissions(false)->setContent($string)->parse()->getContent();
164
		}
165
		return preg_replace_callback('/{{picklist:([a-z0-9_]+)}}/i', fn ($matches) => $this->getRecord() ? $this->getPicklistValue($matches[1]) : $matches[1], $string);
166
	}
167
168
	/**
169
	 * Sets number of sequence.
170
	 *
171
	 * @param int    $reqNo
172
	 * @param string $actualSequence
173
	 *
174
	 * @throws \yii\db\Exception
175
	 */
176
	public function setNumberSequence(int $reqNo, string $actualSequence)
177
	{
178
		$dbCommand = \App\Db::getInstance()->createCommand();
179
		$data = ['cur_sequence' => $actualSequence];
180
		$this->set('cur_sequence', $actualSequence);
181
		if ($value = $this->getRelatedValue()) {
182
			$dbCommand->upsert('u_#__modentity_sequences', ['tabid' => $this->get('tabid'),	'value' => $value,	'cur_id' => $reqNo], ['cur_id' => $reqNo])->execute();
183 1
		} else {
184
			$data['cur_id'] = $reqNo;
185 1
			$this->set('cur_id', $reqNo);
186 1
		}
187
		$dbCommand->update('vtiger_modentity_num', $data, ['tabid' => $this->get('tabid')])->execute();
188 1
	}
189 1
190 1
	/**
191 1
	 * Update module variables sequences.
192 1
	 *
193 1
	 * @param int   $currentId
194 1
	 * @param array $conditions
195 1
	 *
196 1
	 * @return void
197 1
	 */
198 1
	public function updateModuleVariablesSequences($currentId, $conditions = []): void
199 1
	{
200 1
		$conditions['tabid'] = $this->get('tabid');
201 1
		\App\Db::getInstance()->createCommand()->update('u_#__modentity_sequences', ['cur_id' => $currentId], $conditions)->execute();
202 1
	}
203
204
	/**
205
	 * Function to check if record need a new number of sequence.
206
	 *
207
	 * @return bool
208
	 */
209
	public function isNewSequence(): bool
210
	{
211
		return $this->getRecord()->isNew()
212
			|| ($this->getRelatedValue() && $this->getRelatedValue() !== (clone $this)
213
				->setRecord((clone $this->getRecord())->getInstanceByEntity($this->getRecord()->getEntity(), $this->getRecord()->getId()))->getRelatedValue(true));
214
	}
215
216
	/**
217
	 * Updates missing numbers of records.
218
	 *
219
	 * @throws \yii\db\Exception
220
	 *
221
	 * @return array
222
	 */
223
	public function updateRecords()
224
	{
225
		if ($this->isEmpty('id')) {
226
			return [];
227
		}
228
		$moduleModel = \Vtiger_Module_Model::getInstance($this->get('tabid'));
229
		$fieldModel = current($moduleModel->getFieldsByUiType(4));
230
		$returninfo = [];
231
		if ($fieldModel) {
232
			$fieldTable = $fieldModel->getTableName();
233
			$fieldColumn = $fieldModel->getColumnName();
234
			if ($fieldTable === $moduleModel->getEntityInstance()->table_name) {
235
				$picklistName = $this->getPicklistName();
236
				$queryGenerator = new \App\QueryGenerator(\App\Module::getModuleName($this->get('tabid')));
237 1
				$queryGenerator->setFields([$picklistName, $fieldModel->getName(), 'id']);
238
				if (\App\TextParser::isVaribleToParse($this->get('prefix') . $this->get('postfix'))) {
239
					$queryGenerator->setFields(array_keys($queryGenerator->getModuleFields()))->setField('id');
240
				}
241
				$queryGenerator->permissions = false;
242
				$queryGenerator->addNativeCondition(['or', [$fieldColumn => ''], [$fieldColumn => null]]);
243
				$dataReader = $queryGenerator->createQuery()->createCommand()->query();
244
				$totalCount = $dataReader->count();
245
				if ($totalCount) {
246
					$returninfo['totalrecords'] = $totalCount;
247
					$returninfo['updatedrecords'] = 0;
248 11
					$sequenceNumber = $this->get('cur_id');
249
					$oldNumber = $sequenceNumber;
250 11
					$oldSequences = $sequences = (new \App\Db\Query())->select(['value', 'cur_id'])->from('u_#__modentity_sequences')->where(['tabid' => $this->get('tabid')])->createCommand()->queryAllByGroup();
251 11
					$dbCommand = \App\Db::getInstance()->createCommand();
252
					while ($recordInfo = $dataReader->read()) {
253 11
						$this->setRecord($moduleModel->getRecordFromArray($recordInfo));
254
						$seq = 0;
255
						$value = $this->getRelatedValue(true);
256
						if ($value && isset($sequences[$value])) {
257
							$seq = $sequences[$value]++;
258
						} elseif ($value) {
259
							$sequences[$value] = 1;
260
							$seq = $sequences[$value]++;
261 17
						} else {
262
							$seq = $sequenceNumber++;
263 17
						}
264 17
						$dbCommand->update($fieldTable, [$fieldColumn => \App\Purifier::decodeHtml($this->parseNumber($seq))], [$moduleModel->getEntityInstance()->table_index => $recordInfo['id']])
265 3
							->execute();
266 15
						++$returninfo['updatedrecords'];
267 2
					}
268 14
					$dataReader->close();
269 2
					if ($oldNumber != $sequenceNumber) {
270
						$dbCommand->update('vtiger_modentity_num', ['cur_id' => $sequenceNumber], ['tabid' => $this->get('tabid')])->execute();
271 12
					}
272
					foreach (array_diff($sequences, $oldSequences) as $prefix => $num) {
273
						$dbCommand->update('u_#__modentity_sequences', ['cur_id' => $num], ['value' => $prefix, 'tabid' => $this->get('tabid')])
274
							->execute();
275
					}
276
				}
277
			} else {
278
				\App\Log::error('Updating Missing Sequence Number FAILED! REASON: Field table and module table mismatching.');
279
			}
280
		}
281
		return $returninfo;
282 7
	}
283
284 7
	/**
285 7
	 * Date function that can be overrided in tests.
286 2
	 *
287 2
	 * @param string   $format
288 2
	 * @param int|null $time
289 2
	 *
290 2
	 * @return false|string
291 2
	 */
292 2
	public static function date($format, $time = null)
293 2
	{
294 2
		if (null === $time) {
295 2
			$time = time();
296
		}
297 5
		return date($format, $time);
298 5
	}
299 5
300 5
	/**
301 5
	 * Get sequence number that should be saved.
302 5
	 *
303 5
	 * @param string $resetSequence one character
304 5
	 *
305 5
	 * @return string
306
	 */
307
	public static function getSequenceNumber($resetSequence): string
308
	{
309
		switch ($resetSequence) {
310
			case 'Y':
311
				$currentSeqId = static::date('Y');
312
				break;
313
			case 'M':
314
				$currentSeqId = static::date('Ym'); // with year because 2016-10 (10) === 2017-10 (10) and number will be incremented but should be set to 1 (new year)
315
				break;
316
			case 'D':
317
				$currentSeqId = static::date('Ymd'); // same as above because 2016-10-03 (03) === 2016-11-03 (03)
318
				break;
319
			default:
320
				$currentSeqId = '';
321
		}
322
323
		return $currentSeqId;
324
	}
325
326
	/**
327
	 * Saves configuration.
328
	 *
329
	 * @throws \yii\db\Exception
330
	 *
331
	 * @return int
332
	 */
333
	public function save()
334
	{
335
		$dbCommand = \App\Db::getInstance()->createCommand();
336
		if ($this->isEmpty('id')) {
337
			return $dbCommand->insert('vtiger_modentity_num', [
338
				'tabid' => $this->get('tabid'),
339
				'prefix' => $this->get('prefix'),
340
				'leading_zeros' => $this->get('leading_zeros') ?: 0,
341
				'postfix' => $this->get('postfix') ?: '',
342
				'start_id' => $this->get('cur_id'),
343
				'cur_id' => $this->get('cur_id'),
344
				'reset_sequence' => $this->get('reset_sequence'),
345
				'cur_sequence' => $this->get('cur_sequence'),
346
			])->execute();
347
		}
348
		return $dbCommand->update(
349
			'vtiger_modentity_num',
350
			[
351
				'cur_id' => $this->get('cur_id'),
352
				'prefix' => $this->get('prefix'),
353
				'leading_zeros' => $this->get('leading_zeros'),
354
				'postfix' => $this->get('postfix'),
355
				'reset_sequence' => $this->get('reset_sequence'),
356
				'cur_sequence' => $this->get('cur_sequence'), ],
357
			['tabid' => $this->get('tabid')]
358
		)
359
			->execute();
360
	}
361
362
	/**
363
	 * Get sequence number field name.
364
	 *
365
	 * @param int $tabId
366
	 *
367
	 * @return string|bool
368
	 */
369
	public static function getSequenceNumberFieldName(int $tabId)
370
	{
371
		return self::getSequenceNumberField($tabId)['fieldname'] ?? '';
372
	}
373
374
	/**
375
	 * Get sequence number field.
376
	 *
377
	 * @param int $tabId
378
	 *
379
	 * @return string[]|bool
380
	 */
381
	public static function getSequenceNumberField(int $tabId)
382
	{
383
		if (isset(self::$sequenceNumberFieldCache[$tabId])) {
384
			return self::$sequenceNumberFieldCache[$tabId];
385
		}
386
		return self::$sequenceNumberFieldCache[$tabId] = (new \App\Db\Query())->select(['fieldname', 'columnname', 'tablename'])->from('vtiger_field')
387
			->where(['tabid' => $tabId, 'uitype' => 4, 'presence' => [0, 2]])->one();
388
	}
389
390
	/**
391
	 * Clean sequence number field cache.
392
	 *
393
	 * @param int|null $tabId
394
	 *
395
	 * @return void
396
	 */
397
	public static function cleanSequenceNumberFieldCache(?int $tabId)
398
	{
399
		if ($tabId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tabId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
400
			unset(self::$sequenceNumberFieldCache[$tabId]);
401
		}
402
		self::$sequenceNumberFieldCache = null;
403
	}
404
405
	/**
406
	 * Returns name of picklist. Postfix or prefix can contains name of picklist.
407
	 *
408
	 * @return string
409
	 */
410
	private function getPicklistName(): string
411
	{
412
		preg_match('/{{picklist:([a-z0-9_]+)}}/i', $this->get('prefix') . $this->get('postfix'), $matches);
413
		return $matches[1] ?? '';
414
	}
415
416
	/**
417
	 * Returns prefix of picklist.
418
	 *
419
	 * @param string      $picklistName
420
	 * @param string|null $recordValue
421
	 *
422
	 * @return string
423
	 */
424
	private function getPicklistValue(string $picklistName, ?string $recordValue = null): string
425
	{
426
		$values = Picklist::getValues($picklistName);
427
		if (null === $recordValue) {
428
			$recordValue = $this->getRecord()->get($picklistName);
429
		}
430
		foreach ($values as $value) {
431
			if ($recordValue === $value[$picklistName]) {
432
				return $value['prefix'] ?? '';
433
			}
434
		}
435
		return '';
436
	}
437
}
438