Passed
Push — developer ( eb2465...8c7bce )
by Radosław
22:36 queued 06:39
created

RecordNumber::save()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 27
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 27
ccs 0
cts 0
cp 0
rs 9.568
c 0
b 0
f 0
cc 4
nc 2
nop 0
crap 20
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 (isset(self::$instanceCache[$tabId])) {
48
			return self::$instanceCache[$tabId];
49 24
		}
50
		$instance = new static();
51 24
		if (!is_numeric($tabId)) {
52
			$tabId = \App\Module::getModuleId($tabId);
53
		}
54
		$row = (new \App\Db\Query())->from('vtiger_modentity_num')->where(['tabid' => $tabId])->one() ?: [];
55
		$row['tabid'] = $tabId;
56
		$instance->setData($row);
57
		return self::$instanceCache[$tabId] = $instance;
58
	}
59 16
60
	/**
61 16
	 * Sets model of record.
62 16
	 *
63 4
	 * @param \Vtiger_Record_Model $recordModel
64
	 *
65 16
	 * @return $this
66
	 */
67
	public function setRecord(\Vtiger_Record_Model $recordModel): self
68 16
	{
69 16
		$this->set('recordModel', $recordModel);
70 16
		return $this;
71 4
	}
72
73 16
	/**
74 16
	 * Returns model of record.
75
	 *
76 16
	 * @return \Vtiger_Record_Model|null
77 16
	 */
78
	public function getRecord(): ?\Vtiger_Record_Model
79
	{
80
		return $this->get('recordModel');
81
	}
82
83
	/**
84
	 * Function to get the next nuber of record.
85 16
	 *
86
	 * @return string
87 16
	 */
88 16
	public function getIncrementNumber(): string
89
	{
90
		$actualSequence = static::getSequenceNumber($this->get('reset_sequence'));
91
		if ($this->get('reset_sequence') && $this->get('cur_sequence') !== $actualSequence) {
92
			$currentSequenceNumber = 1;
93
			$this->updateModuleVariablesSequences($currentSequenceNumber);
94
		} else {
95
			$currentSequenceNumber = $this->getCurrentSequenceNumber();
96
		}
97
98 29
		$fullPrefix = $this->parseNumber($currentSequenceNumber);
99
		$strip = \strlen($currentSequenceNumber) - \strlen($currentSequenceNumber + 1);
100 29
		if ($strip < 0) {
101 29
			$strip = 0;
102
		}
103
		$temp = str_repeat('0', $strip);
104
		$reqNo = $temp . ($currentSequenceNumber + 1);
105
106
		$this->setNumberSequence($reqNo, $actualSequence);
0 ignored issues
show
Bug introduced by
$reqNo of type string is incompatible with the type integer expected by parameter $reqNo of App\Fields\RecordNumber::setNumberSequence(). ( Ignorable by Annotation )

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

106
		$this->setNumberSequence(/** @scrutinizer ignore-type */ $reqNo, $actualSequence);
Loading history...
107
		return \App\Purifier::decodeHtml($fullPrefix);
108
	}
109
110
	/**
111 17
	 * Gets related value.
112
	 *
113
	 * @param bool $reload
114
	 *
115 17
	 * @return string
116
	 */
117
	public function getRelatedValue(bool $reload = false): string
118
	{
119
		if (!isset($this->relatedValue) || $reload) {
120
			$value = [];
121
			preg_match_all('/{{picklist:([a-zA-Z0-9_]+)}}|\$\((\w+) : ([,"\+\#\%\.\=\-\[\]\&\w\s\|\)\(\:]+)\)\$/u', $this->get('prefix') . $this->get('postfix'), $matches);
122
			if ($this->getRecord() && !empty($matches[0])) {
123
				foreach ($matches[0] as $key => $element) {
124
					if (0 === strpos($element, '{{picklist:')) {
125
						$value[] = $this->getPicklistValue($matches[1][$key]);
126 16
					} else {
127
						$value[] = \App\TextParser::getInstanceByModel($this->getRecord())->setGlobalPermissions(false)->setContent($element)->parse()->getContent();
128 16
					}
129 16
				}
130 16
			}
131 16
			$this->relatedValue = implode('|', $value);
132 16
		}
133
		return $this->relatedValue;
134
	}
135
136
	/**
137
	 * Parse number based on postfix and prefix.
138
	 *
139
	 * @param int $seq
140 16
	 *
141 16
	 * @return string
142
	 */
143
	public function parseNumber(int $seq): string
144
	{
145
		$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'));
146
		if ($this->getRecord()) {
147
			$string = \App\TextParser::getInstanceByModel($this->getRecord())->setGlobalPermissions(false)->setContent($string)->parse()->getContent();
148 24
		}
149
		return preg_replace_callback('/{{picklist:([a-z0-9_]+)}}/i', fn ($matches) => $this->getRecord() ? $this->getPicklistValue($matches[1]) : $matches[1], $string);
150 24
	}
151 24
152
	/**
153
	 * Sets number of sequence.
154
	 *
155
	 * @param int    $reqNo
156
	 * @param string $actualSequence
157
	 *
158
	 * @throws \yii\db\Exception
159
	 */
160
	public function setNumberSequence(int $reqNo, string $actualSequence)
161
	{
162
		$data = ['cur_sequence' => $actualSequence];
163
		$this->set('cur_sequence', $actualSequence);
164
		if ($value = $this->getRelatedValue()) {
165
			$this->updateNumberSequence($reqNo, $value);
166
		} else {
167
			$data['cur_id'] = $reqNo;
168
			$this->set('cur_id', $reqNo);
169
		}
170
		\App\Db::getInstance()->createCommand()->update('vtiger_modentity_num', $data, ['tabid' => $this->get('tabid')])->execute();
171
	}
172
173
	/**
174
	 * Update number sequence.
175
	 *
176
	 * @param int    $reqNo
177
	 * @param string $prefix
178
	 *
179
	 * @return bool|int
180
	 */
181
	public function updateNumberSequence(int $reqNo, string $prefix)
182
	{
183 1
		if ((new \App\Db\Query())->from('u_#__modentity_sequences')->where(['value' => $prefix, 'tabid' => $this->get('tabid')])->exists()) {
184
			$value = \App\Db::getInstance()->createCommand()->update('u_#__modentity_sequences', ['cur_id' => $reqNo], ['value' => $prefix, 'tabid' => $this->get('tabid')])->execute();
185 1
		} else {
186 1
			$value = \App\Db::getInstance()->createCommand()->insert('u_#__modentity_sequences', ['cur_id' => $reqNo, 'tabid' => $this->get('tabid'), 'value' => $prefix])->execute();
187
		}
188 1
		return $value;
189 1
	}
190 1
191 1
	/**
192 1
	 * Update all module variables sequences.
193 1
	 *
194 1
	 * @param string $currentId
195 1
	 *
196 1
	 * @return void
197 1
	 */
198 1
	public function updateModuleVariablesSequences($currentId): void
199 1
	{
200 1
		\App\Db::getInstance()->createCommand()->update('u_#__modentity_sequences', ['cur_id' => $currentId], ['tabid' => $this->get('tabid')])->execute();
201 1
	}
202 1
203
	/**
204
	 * Function to check if record need a new number of sequence.
205
	 *
206
	 * @return bool
207
	 */
208
	public function isNewSequence(): bool
209
	{
210
		return $this->getRecord()->isNew()
211
			|| ($this->getRelatedValue() && $this->getRelatedValue() !== self::getInstance($this->getRecord()->getModuleName())
212
				->setRecord((clone $this->getRecord())->getInstanceByEntity($this->getRecord()->getEntity(), $this->getRecord()->getId()))->getRelatedValue());
213
	}
214
215
	/**
216
	 * Updates missing numbers of records.
217
	 *
218
	 * @throws \yii\db\Exception
219
	 *
220
	 * @return array
221
	 */
222
	public function updateRecords()
223
	{
224
		if ($this->isEmpty('id')) {
225
			return [];
226
		}
227
		$moduleModel = \Vtiger_Module_Model::getInstance($this->get('tabid'));
228
		$fieldModel = current($moduleModel->getFieldsByUiType(4));
229
		$returninfo = [];
230
		if ($fieldModel) {
231
			$fieldTable = $fieldModel->getTableName();
232
			$fieldColumn = $fieldModel->getColumnName();
233
			if ($fieldTable === $moduleModel->getEntityInstance()->table_name) {
234
				$picklistName = $this->getPicklistName();
235
				$queryGenerator = new \App\QueryGenerator(\App\Module::getModuleName($this->get('tabid')));
236
				$queryGenerator->setFields([$picklistName, $fieldModel->getName(), 'id']);
237 1
				if (\App\TextParser::isVaribleToParse($this->get('prefix') . $this->get('postfix'))) {
238
					$queryGenerator->setFields(array_keys($queryGenerator->getModuleFields()))->setField('id');
239
				}
240
				$queryGenerator->permissions = false;
241
				$queryGenerator->addNativeCondition(['or', [$fieldColumn => ''], [$fieldColumn => null]]);
242
				$dataReader = $queryGenerator->createQuery()->createCommand()->query();
243
				$totalCount = $dataReader->count();
244
				if ($totalCount) {
245
					$returninfo['totalrecords'] = $totalCount;
246
					$returninfo['updatedrecords'] = 0;
247
					$sequenceNumber = $this->get('cur_id');
248 11
					$oldNumber = $sequenceNumber;
249
					$oldSequences = $sequences = (new \App\Db\Query())->select(['value', 'cur_id'])->from('u_#__modentity_sequences')->where(['tabid' => $this->get('tabid')])->createCommand()->queryAllByGroup();
250 11
					$dbCommand = \App\Db::getInstance()->createCommand();
251 11
					while ($recordInfo = $dataReader->read()) {
252
						$this->setRecord($moduleModel->getRecordFromArray($recordInfo));
253 11
						$seq = 0;
254
						$value = $this->getRelatedValue(true);
255
						if ($value && isset($sequences[$value])) {
256
							$seq = $sequences[$value]++;
257
						} elseif ($value) {
258
							$sequences[$value] = 1;
259
							$seq = $sequences[$value]++;
260
						} else {
261 17
							$seq = $sequenceNumber++;
262
						}
263 17
						$dbCommand->update($fieldTable, [$fieldColumn => \App\Purifier::decodeHtml($this->parseNumber($seq))], [$moduleModel->getEntityInstance()->table_index => $recordInfo['id']])
264 17
							->execute();
265 3
						++$returninfo['updatedrecords'];
266 15
					}
267 2
					$dataReader->close();
268 14
					if ($oldNumber != $sequenceNumber) {
269 2
						$dbCommand->update('vtiger_modentity_num', ['cur_id' => $sequenceNumber], ['tabid' => $this->get('tabid')])->execute();
270
					}
271 12
					foreach (array_diff($sequences, $oldSequences) as $prefix => $num) {
272
						$dbCommand->update('u_#__modentity_sequences', ['cur_id' => $num], ['value' => $prefix, 'tabid' => $this->get('tabid')])
273
							->execute();
274
					}
275
				}
276
			} else {
277
				\App\Log::error('Updating Missing Sequence Number FAILED! REASON: Field table and module table mismatching.');
278
			}
279
		}
280
		return $returninfo;
281
	}
282 7
283
	/**
284 7
	 * Date function that can be overrided in tests.
285 7
	 *
286 2
	 * @param string   $format
287 2
	 * @param int|null $time
288 2
	 *
289 2
	 * @return false|string
290 2
	 */
291 2
	public static function date($format, $time = null)
292 2
	{
293 2
		if (null === $time) {
294 2
			$time = time();
295 2
		}
296
		return date($format, $time);
297 5
	}
298 5
299 5
	/**
300 5
	 * Get sequence number that should be saved.
301 5
	 *
302 5
	 * @param string $resetSequence one character
303 5
	 *
304 5
	 * @return string
305 5
	 */
306
	public static function getSequenceNumber($resetSequence): string
307
	{
308
		switch ($resetSequence) {
309
			case 'Y':
310
				$currentSeqId = static::date('Y');
311
				break;
312
			case 'M':
313
				$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)
314
				break;
315
			case 'D':
316
				$currentSeqId = static::date('Ymd'); // same as above because 2016-10-03 (03) === 2016-11-03 (03)
317
				break;
318
			default:
319
				$currentSeqId = '';
320
		}
321
322
		return $currentSeqId;
323
	}
324
325
	/**
326
	 * Saves configuration.
327
	 *
328
	 * @throws \yii\db\Exception
329
	 *
330
	 * @return int
331
	 */
332
	public function save()
333
	{
334
		$dbCommand = \App\Db::getInstance()->createCommand();
335
		if ($this->isEmpty('id')) {
336
			return $dbCommand->insert('vtiger_modentity_num', [
337
				'tabid' => $this->get('tabid'),
338
				'prefix' => $this->get('prefix'),
339
				'leading_zeros' => $this->get('leading_zeros') ?: 0,
340
				'postfix' => $this->get('postfix') ?: '',
341
				'start_id' => $this->get('cur_id'),
342
				'cur_id' => $this->get('cur_id'),
343
				'reset_sequence' => $this->get('reset_sequence'),
344
				'cur_sequence' => $this->get('cur_sequence'),
345
			])->execute();
346
		}
347
		return $dbCommand->update(
348
			'vtiger_modentity_num',
349
			[
350
				'cur_id' => $this->get('cur_id'),
351
				'prefix' => $this->get('prefix'),
352
				'leading_zeros' => $this->get('leading_zeros'),
353
				'postfix' => $this->get('postfix'),
354
				'reset_sequence' => $this->get('reset_sequence'),
355
				'cur_sequence' => $this->get('cur_sequence'), ],
356
			['tabid' => $this->get('tabid')]
357
		)
358
			->execute();
359
	}
360
361
	/**
362
	 * Get sequence number field name.
363
	 *
364
	 * @param int $tabId
365
	 *
366
	 * @return string|bool
367
	 */
368
	public static function getSequenceNumberFieldName(int $tabId)
369
	{
370
		return self::getSequenceNumberField($tabId)['fieldname'] ?? '';
371
	}
372
373
	/**
374
	 * Get sequence number field.
375
	 *
376
	 * @param int $tabId
377
	 *
378
	 * @return string[]|bool
379
	 */
380
	public static function getSequenceNumberField(int $tabId)
381
	{
382
		if (isset(self::$sequenceNumberFieldCache[$tabId])) {
383
			return self::$sequenceNumberFieldCache[$tabId];
384
		}
385
		return self::$sequenceNumberFieldCache[$tabId] = (new \App\Db\Query())->select(['fieldname', 'columnname', 'tablename'])->from('vtiger_field')
386
			->where(['tabid' => $tabId, 'uitype' => 4, 'presence' => [0, 2]])->one();
387
	}
388
389
	/**
390
	 * Clean sequence number field cache.
391
	 *
392
	 * @param int|null $tabId
393
	 *
394
	 * @return void
395
	 */
396
	public static function cleanSequenceNumberFieldCache(?int $tabId)
397
	{
398
		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...
399
			unset(self::$sequenceNumberFieldCache[$tabId]);
400
		}
401
		self::$sequenceNumberFieldCache = null;
402
	}
403
404
	/**
405
	 * Function to get current sequence number.
406
	 *
407
	 * @return int
408
	 */
409
	private function getCurrentSequenceNumber(): int
410
	{
411
		$seq = $this->get('cur_id');
412
		if ($value = $this->getRelatedValue()) {
413
			$seq = (new \App\Db\Query())->select(['cur_id'])->from('u_#__modentity_sequences')->where(['tabid' => $this->get('tabid'), 'value' => $value])->scalar() ?: 1;
414
		}
415
		return $seq;
416
	}
417
418
	/**
419
	 * Returns name of picklist. Postfix or prefix can contains name of picklist.
420
	 *
421
	 * @return string
422
	 */
423
	private function getPicklistName(): string
424
	{
425
		preg_match('/{{picklist:([a-z0-9_]+)}}/i', $this->get('prefix') . $this->get('postfix'), $matches);
426
		return $matches[1] ?? '';
427
	}
428
429
	/**
430
	 * Returns prefix of picklist.
431
	 *
432
	 * @param string      $picklistName
433
	 * @param string|null $recordValue
434
	 *
435
	 * @return string
436
	 */
437
	private function getPicklistValue(string $picklistName, ?string $recordValue = null): string
438
	{
439
		$values = Picklist::getValues($picklistName);
440
		if (null === $recordValue) {
441
			$recordValue = $this->getRecord()->get($picklistName);
442
		}
443
		foreach ($values as $value) {
444
			if ($recordValue === $value[$picklistName]) {
445
				return $value['prefix'] ?? '';
446
			}
447
		}
448
		return '';
449
	}
450
}
451