Test Setup Failed
Push — developer ( 4955d5...3912ea )
by Mariusz
16:57
created

Synchronizer::runQueue()   B

Complexity

Conditions 7
Paths 19

Size

Total Lines 37
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 32
c 1
b 0
f 0
dl 0
loc 37
rs 8.4746
cc 7
nc 19
nop 1
1
<?php
2
/**
3
 * Comarch base method synchronization 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 Integration
9
 *
10
 * @copyright YetiForce S.A.
11
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
12
 * @author    Mariusz Krzaczkowski <[email protected]>
13
 */
14
15
namespace App\Integrations\Comarch;
16
17
/**
18
 * Comarch abstract base method synchronization class.
19
 */
20
class Synchronizer
21
{
22
	/** @var string Category name used for the log mechanism */
23
	const LOG_CATEGORY = 'Integrations/Comarch';
24
	/** @var string The name of the configuration parameter for rows limit */
25
	const LIMIT_NAME = '';
26
	/** @var int Synchronization direction: one-way from Comarch to YetiForce */
27
	const DIRECTION_API_TO_YF = 0;
28
	/** @var int Synchronization direction: one-way from YetiForce to Comarch */
29
	const DIRECTION_YF_TO_API = 1;
30
	/** @var int Synchronization direction: two-way */
31
	const DIRECTION_TWO_WAY = 2;
32
	/** @var \App\Integrations\Comarch\Config Config instance. */
33
	public $config;
34
	/** @var \App\Integrations\Comarch Controller instance. */
35
	public $controller;
36
	/** @var string Synchronizer name. */
37
	protected $name;
38
	/** @var \App\Integrations\Comarch\Connector\Base Connector. */
39
	protected $connector;
40
	/** @var \App\Integrations\Comarch\Map[] Map synchronizer instances. */
41
	protected $maps;
42
	/** @var array Last scan config data. */
43
	protected $lastScan = [];
44
	/** @var int[] Imported ids */
45
	protected $imported = [];
46
	/** @var int[] Exported ids */
47
	protected $exported = [];
48
49
	/**
50
	 * Constructor.
51
	 *
52
	 * @param \App\Integrations\Comarch $controller
53
	 */
54
	public function __construct(\App\Integrations\Comarch $controller)
55
	{
56
		$this->name = substr(strrchr(static::class, '\\'), 1);
57
		$this->connector = $controller->getConnector();
58
		$this->controller = $controller;
59
		$this->config = $controller->config;
60
	}
61
62
	/**
63
	 * Main process function.
64
	 * Required for master synchronizers, not required for dependent ones.
65
	 *
66
	 * @return void
67
	 */
68
	public function process(): void
69
	{
70
		throw new \App\Exceptions\AppException('Function not implemented');
71
	}
72
73
	/**
74
	 * Get map model instance.
75
	 *
76
	 * @param string $name
77
	 *
78
	 * @return \App\Integrations\Comarch\Map
79
	 */
80
	public function getMapModel(string $name = ''): Map
81
	{
82
		if (empty($name)) {
83
			$name = rtrim($this->name, 's');
84
		}
85
		if (isset($this->maps[$name])) {
86
			return $this->maps[$name];
87
		}
88
		$className = 'App\\Integrations\\Comarch\\' . $this->config->get('connector') . "\\Maps\\{$name}";
89
		if (isset($this->config->get('maps')[$name])) {
90
			$className = $this->config->get('maps')[$name];
91
		}
92
		return $this->maps[$name] = new $className($this);
93
	}
94
95
	/**
96
	 * Get data by path from API.
97
	 *
98
	 * @param string $path
99
	 * @param bool   $cache
100
	 *
101
	 * @return array
102
	 */
103
	public function getFromApi(string $path, bool $cache = true): array
104
	{
105
		$cacheKey = $this::LOG_CATEGORY . '/API';
106
		if ($cache && \App\Cache::staticHas($cacheKey, $path)) {
107
			return \App\Cache::staticGet($cacheKey, $path);
108
		}
109
		$data = \App\Json::decode($this->connector->request('GET', $path));
110
		\App\Cache::staticSave($cacheKey, $path, $data);
111
		if ($this->config->get('log_all')) {
112
			$this->controller->log('Get from API', [
113
				'path' => $path,
114
				'rows' => \count($data),
115
			]);
116
		}
117
		return $data;
118
	}
119
120
	/**
121
	 * Get QueryGenerator to retrieve data from YF.
122
	 *
123
	 * @param string $moduleName
124
	 * @param bool   $filterByDate
125
	 *
126
	 * @return \App\QueryGenerator
127
	 */
128
	public function getFromYf(string $moduleName, bool $filterByDate = false): \App\QueryGenerator
129
	{
130
		$queryGenerator = new \App\QueryGenerator($moduleName);
131
		$queryGenerator->setStateCondition('All');
132
		$queryGenerator->setFields(['id'])->permissions = false;
133
		$queryGenerator->addCondition('comarch_server_id', $this->config->get('id'), 'e');
134
		if ($filterByDate) {
135
			if (!empty($this->lastScan['start_date'])) {
136
				$queryGenerator->addNativeCondition(['<', 'vtiger_crmentity.modifiedtime', $this->lastScan['start_date']]);
137
			}
138
			if (!empty($this->lastScan['end_date'])) {
139
				$queryGenerator->addNativeCondition(['>', 'vtiger_crmentity.modifiedtime', $this->lastScan['end_date']]);
140
			}
141
		}
142
		return $queryGenerator;
143
	}
144
145
	/**
146
	 * Method to get search conditions in the Comarch API.
147
	 *
148
	 * @return string
149
	 */
150
	public function getFromApiCond(): string
151
	{
152
		$searchCriteria = [];
153
		if (!empty($this->lastScan['start_date'])) {
154
			$searchCriteria[] = 'dataCzasModyfikacjiDo=' . $this->getFormattedTime($this->lastScan['start_date']);
155
		}
156
		if (!empty($this->lastScan['end_date'])) {
157
			$searchCriteria[] = 'dataCzasModyfikacjiOd=' . $this->getFormattedTime($this->lastScan['end_date']);
158
		}
159
		$searchCriteria[] = 'limit=' . $this->config->get($this::LIMIT_NAME);
160
		$searchCriteria = implode('&', $searchCriteria);
161
		return $searchCriteria ?? '';
162
	}
163
164
	/**
165
	 * Get YF id by API id.
166
	 *
167
	 * @param int         $apiId
168
	 * @param string|null $moduleName
169
	 *
170
	 * @return int|null
171
	 */
172
	public function getYfId(int $apiId, ?string $moduleName = null): ?int
173
	{
174
		$moduleName ??= $this->getMapModel()->getModule();
175
		$cacheKey = 'Integrations/Comarch/CRM_ID/' . $moduleName;
176
		if (\App\Cache::staticHas($cacheKey, $apiId)) {
177
			return \App\Cache::staticGet($cacheKey, $apiId);
178
		}
179
		$queryGenerator = $this->getFromYf($moduleName);
180
		$queryGenerator->addCondition($this->getMapModel()::FIELD_NAME_ID, $apiId, 'e');
181
		$yfId = $queryGenerator->createQuery()->scalar() ?: null;
182
		if (null !== $yfId) {
183
			$this->updateMapIdCache($moduleName, $apiId, $yfId);
0 ignored issues
show
Bug introduced by
$yfId of type string is incompatible with the type integer expected by parameter $yfId of App\Integrations\Comarch...zer::updateMapIdCache(). ( Ignorable by Annotation )

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

183
			$this->updateMapIdCache($moduleName, $apiId, /** @scrutinizer ignore-type */ $yfId);
Loading history...
184
		}
185
		return $yfId;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $yfId 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...
186
	}
187
188
	/**
189
	 * Get YF id by API id.
190
	 *
191
	 * @param int     $yfId
192
	 * @param ?string $moduleName
193
	 * @param mixed   $apiValue
194
	 * @param array   $field
195
	 *
196
	 * @return int
197
	 */
198
	public function getApiId(int $yfId, ?string $moduleName = null): int
199
	{
200
		$moduleName ??= $this->getMapModel()->getModule();
201
		$cacheKey = 'Integrations/Comarch/API_ID/' . $moduleName;
202
		if (\App\Cache::staticHas($cacheKey, $yfId)) {
203
			return \App\Cache::staticGet($cacheKey, $yfId);
204
		}
205
		$apiId = 0;
206
		try {
207
			$recordModel = \Vtiger_Record_Model::getInstanceById($yfId, $moduleName);
208
			$apiId = $recordModel->get('comarch_id') ?: 0;
209
		} catch (\Throwable $th) {
210
			$this->logError('GetApiId ' . $this->name, ['comarch_id' => $yfId, 'moduleName' => $moduleName], $th);
211
		}
212
		$this->updateMapIdCache($moduleName, $apiId, $yfId);
213
		return $apiId;
214
	}
215
216
	/**
217
	 * Get YF value by API value.
218
	 *
219
	 * @param mixed $apiValue
220
	 * @param array $field
221
	 *
222
	 * @return mixed
223
	 */
224
	public function getYfValue($apiValue, array $field)
0 ignored issues
show
Unused Code introduced by
The parameter $apiValue is not used and could be removed. ( Ignorable by Annotation )

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

224
	public function getYfValue(/** @scrutinizer ignore-unused */ $apiValue, array $field)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

224
	public function getYfValue($apiValue, /** @scrutinizer ignore-unused */ array $field)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
225
	{
226
		return '';
227
	}
228
229
	/**
230
	 * Get YF value by API value.
231
	 *
232
	 * @param mixed $yfValue
233
	 * @param array $field
234
	 *
235
	 * @return mixed
236
	 */
237
	public function getApiValue($yfValue, array $field)
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

237
	public function getApiValue($yfValue, /** @scrutinizer ignore-unused */ array $field)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $yfValue is not used and could be removed. ( Ignorable by Annotation )

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

237
	public function getApiValue(/** @scrutinizer ignore-unused */ $yfValue, array $field)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
238
	{
239
		return '';
240
	}
241
242
	/**
243
	 * Update the identifier mapping of both systems.
244
	 *
245
	 * @param string $moduleName
246
	 * @param int    $apiId
247
	 * @param int    $yfId
248
	 *
249
	 * @return void
250
	 */
251
	public function updateMapIdCache(string $moduleName, int $apiId, int $yfId): void
252
	{
253
		\App\Cache::staticSave('Integrations/Comarch/API_ID/' . $moduleName, $yfId, $apiId);
254
		\App\Cache::staticSave('Integrations/Comarch/CRM_ID/' . $moduleName, $apiId, $yfId);
255
	}
256
257
	/**
258
	 * Return parsed time to Comarch time zone.
259
	 *
260
	 * @param string $value
261
	 *
262
	 * @return string
263
	 */
264
	public function getFormattedTime(string $value): string
265
	{
266
		return \DateTimeField::convertTimeZone($value, \App\Fields\DateTime::getTimeZone(), 'GMT+2')->format('Y-m-d\\TH:i:s');
267
	}
268
269
	/**
270
	 * Export items to Comarch.
271
	 *
272
	 * @return void
273
	 */
274
	public function export(): void
275
	{
276
		$this->lastScan = $this->config->getLastScan('export' . $this->name);
277
		if (
278
			!$this->lastScan['start_date']
279
			|| (0 === $this->lastScan['id'] && $this->lastScan['start_date'] === $this->lastScan['end_date'])
280
		) {
281
			$this->config->setScan('export' . $this->name);
282
			$this->lastScan = $this->config->getLastScan('export' . $this->name);
283
		}
284
		if ($this->config->get('log_all')) {
285
			$this->controller->log('Start export ' . $this->name, [
286
				'lastScan' => $this->lastScan,
287
			]);
288
		}
289
		$i = 0;
290
		try {
291
			$page = $this->lastScan['page'] ?? 0;
292
			$load = true;
293
			$finish = false;
294
			$query = $this->getExportQuery();
295
			$limit = $this->config->get($this::LIMIT_NAME);
296
			while ($load) {
297
				$query->offset($page);
298
				if ($rows = $query->all()) {
299
					foreach ($rows as $row) {
300
						$this->exportItem($row['id']);
301
						$this->config->setScan('export' . $this->name, 'id', $row['id']);
302
						++$i;
303
					}
304
					++$page;
305
					if (\is_callable($this->controller->bathCallback)) {
306
						$load = \call_user_func($this->controller->bathCallback, 'export' . $this->name);
307
					}
308
					if ($limit !== \count($rows)) {
309
						$finish = true;
310
					}
311
				} else {
312
					$finish = true;
313
				}
314
				if ($finish || !$load) {
315
					$load = false;
316
					if ($finish) {
317
						$this->config->setEndScan('export' . $this->name, $this->lastScan['start_date']);
318
					} else {
319
						$this->config->setScan('export' . $this->name, 'page', $page);
320
					}
321
				}
322
			}
323
		} catch (\Throwable $ex) {
324
			$this->logError('export ' . $this->name, ['API' => $rows ?? ''], $ex);
325
		}
326
		if ($this->config->get('log_all')) {
327
			$this->controller->log('End export ' . $this->name, ['exported' => $i]);
328
		}
329
	}
330
331
	/**
332
	 * Export item to Comarch from YetiFoce.
333
	 *
334
	 * @param int $id
335
	 *
336
	 * @return bool
337
	 */
338
	public function exportItem(int $id): bool
339
	{
340
		$mapModel = $this->getMapModel();
341
		$mapModel->setDataApi([]);
342
		$mapModel->setDataYfById($id);
343
		$mapModel->loadModeApi();
344
		$row = $mapModel->getDataYf('fieldMap', false);
345
		$dataApi = $mapModel->getDataApi();
346
		if ($mapModel->skip) {
347
			if ($this->config->get('log_all')) {
348
				$this->controller->log(
349
					$this->name . ' ' . __FUNCTION__ . ' | skipped , inconsistent data',
350
					['YF' => $row, 'API' => $dataApi ?? []]
351
				);
352
			}
353
		} elseif (empty($dataApi)) {
354
			\App\Log::error(__FUNCTION__ . ' | Empty map details', $this::LOG_CATEGORY);
355
			$this->controller->log(
356
				$this->name . ' ' . __FUNCTION__ . ' | Empty map details',
357
				['YF' => $row, 'API' => $dataApi ?? []],
358
				null,
359
				true
360
			);
361
		} else {
362
			try {
363
				if ('create' === $mapModel->getModeApi() || empty($this->imported[$row[$mapModel::FIELD_NAME_ID]])) {
364
					$mapModel->saveInApi();
365
					$dataApi = $mapModel->getDataApi(false);
366
					$this->exported[$id] = $mapModel->getRecordModel()->get($mapModel::FIELD_NAME_ID);
367
				} else {
368
					$this->updateMapIdCache(
369
						$mapModel->getRecordModel()->getModuleName(),
370
						$mapModel->getRecordModel()->get($mapModel::FIELD_NAME_ID),
371
						$id
372
					);
373
				}
374
				$status = true;
375
			} catch (\Throwable $ex) {
376
				$this->logError(__FUNCTION__ . ' ' . $this->name, ['YF' => $row, 'API' => $dataApi], $ex);
377
				$this->addToQueue('export', $id);
378
				$mapModel->setErrorLog([['message' => 'ERR_ERROR_WHILE_SENDING_DATA']]);
379
				$mapModel->getRecordModel()->save();
380
			}
381
		}
382
		if ($this->config->get('log_all')) {
383
			$this->controller->log(
384
				$this->name . ' ' . __FUNCTION__ . ' | ' .
385
				(\array_key_exists($id, $this->exported) ? 'exported' : 'skipped'),
386
				[
387
					'YF' => $row,
388
					'API' => $dataApi ?? [],
389
				]
390
			);
391
		}
392
		return $status ?? false;
393
	}
394
395
	/**
396
	 * Import account from Comarch to YetiFoce.
397
	 *
398
	 * @param array $row
399
	 *
400
	 * @return bool
401
	 */
402
	public function importItem(array $row): bool
403
	{
404
		$mapModel = $this->getMapModel();
405
		$mapModel->setDataApi($row);
406
		$apiId = $row[$mapModel::API_NAME_ID];
407
		if ($dataYf = $mapModel->getDataYf()) {
408
			try {
409
				$yfId = $mapModel->findRecordInYf();
410
				if (empty($yfId) || empty($this->exported[$yfId])) {
411
					$mapModel->loadRecordModel($yfId);
412
					$mapModel->loadAdditionalData();
413
					$mapModel->saveInYf();
414
					$dataYf['id'] = $this->imported[$apiId] = $mapModel->getRecordModel()->getId();
415
				}
416
				if (!empty($apiId)) {
417
					$this->updateMapIdCache(
418
						$mapModel->getModule(),
419
						$apiId,
420
						$yfId ?: $mapModel->getRecordModel()->getId()
421
					);
422
				}
423
				$status = true;
424
			} catch (\Throwable $ex) {
425
				$this->logError(__FUNCTION__ . ' ' . $this->name, ['YF' => $dataYf, 'API' => $row], $ex);
426
				$this->addToQueue('import', $apiId);
427
			}
428
		} else {
429
			\App\Log::error('Empty map details in ' . __FUNCTION__, $this::LOG_CATEGORY);
430
		}
431
		if ($this->config->get('log_all')) {
432
			$this->controller->log($this->name . ' ' . __FUNCTION__ . ' | ' .
433
			 (\array_key_exists($apiId, $this->imported) ? 'imported' : 'skipped'), [
434
			 	'API' => $row,
435
			 	'YF' => $dataYf ?? [],
436
			 ]);
437
		}
438
		return $status ?? false;
439
	}
440
441
	/**
442
	 * Import by API id.
443
	 *
444
	 * @param int $apiId
445
	 *
446
	 * @return int
447
	 */
448
	public function importById(int $apiId): int
449
	{
450
		throw new \App\Exceptions\AppException('Function not implemented');
451
	}
452
453
	/**
454
	 * Add import/export jobs to the queue.
455
	 *
456
	 * @param string $type
457
	 * @param int    $id
458
	 *
459
	 * @return void
460
	 */
461
	public function addToQueue(string $type, int $id): void
462
	{
463
		$data = ['server_id' => $this->config->get('id'),
464
			'name' => $this->name, 'type' => $type,	'value' => $id,
465
		];
466
		$db = \App\Db::getInstance('admin');
467
		if ((new \App\Db\Query())->from(\App\Integrations\Comarch::QUEUE_TABLE_NAME)
468
			->where(['server_id' => $this->config->get('id'), 'name' => $this->name, 'type' => $type])->exists($db)) {
469
			return;
470
		}
471
		$db->createCommand()->insert(\App\Integrations\Comarch::QUEUE_TABLE_NAME, $data)->execute();
472
	}
473
474
	/**
475
	 * Run import/export jobs from the queue.
476
	 *
477
	 * @param string $type
478
	 *
479
	 * @return void
480
	 */
481
	public function runQueue(string $type): void
482
	{
483
		$db = \App\Db::getInstance('admin');
484
		$dataReader = (new \App\Db\Query())->from(\App\Integrations\Comarch::QUEUE_TABLE_NAME)
485
			->where(['server_id' => $this->config->get('id'), 'name' => $this->name, 'type' => $type])
486
			->createCommand(\App\Db::getInstance('admin'))->query();
487
		while ($row = $dataReader->read()) {
488
			switch ($type) {
489
				case 'export':
490
					$status = $this->exportItem($row['value']);
491
					break;
492
				case 'import':
493
					$status = empty($this->importById($row['value']));
494
					break;
495
				default:
496
					break;
497
			}
498
			$delete = false;
499
			if ($status) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $status does not seem to be defined for all execution paths leading up to this point.
Loading history...
500
				$delete = true;
501
			} else {
502
				$counter = ((int) $row['counter']) + 1;
503
				if (4 === $counter) {
504
					$delete = true;
505
				} else {
506
					$db->createCommand()->update(
507
						\App\Integrations\Comarch::QUEUE_TABLE_NAME,
508
						['counter' => $counter],
509
						['id' => $row['id']]
510
					)->execute();
511
				}
512
			}
513
			if ($delete) {
514
				$db->createCommand()->delete(
515
					\App\Integrations\Comarch::QUEUE_TABLE_NAME,
516
					['id' => $row['id']]
517
				)->execute();
518
			}
519
		}
520
	}
521
522
	/**
523
	 * Get export query.
524
	 *
525
	 * @return \App\Db\Query
526
	 */
527
	protected function getExportQuery(): \App\Db\Query
528
	{
529
		$queryGenerator = $this->getFromYf($this->getMapModel()->getModule(), true);
530
		$queryGenerator->setLimit($this->config->get($this::LIMIT_NAME));
531
		return $queryGenerator->createQuery();
532
	}
533
534
	/**
535
	 * Error logging.
536
	 *
537
	 * @param string     $title
538
	 * @param array|null $params
539
	 * @param \Throwable $ex
540
	 *
541
	 * @return void
542
	 */
543
	protected function logError(string $title, ?array $params, \Throwable $ex): void
544
	{
545
		$this->controller->log($title, $params, $ex);
546
		\App\Log::error("Error during {$title}: \n{$ex->__toString()}", $this::LOG_CATEGORY);
547
	}
548
}
549