Passed
Push — developer ( b6ebe7...0bf5e9 )
by Mariusz
47:54 queued 29:05
created

Synchronizer::export()   F

Complexity

Conditions 15
Paths 296

Size

Total Lines 55
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 41
dl 0
loc 55
rs 3.8833
c 1
b 0
f 0
cc 15
nc 296
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Synchronizer name. */
23
	protected $name;
24
	/** @var \App\Integrations\Comarch\Connector\Base Connector. */
25
	protected $connector;
26
	/** @var \App\Integrations\Comarch\Map[] Map synchronizer instances. */
27
	protected $maps;
28
	/** @var \App\Integrations\Comarch\Config Config instance. */
29
	public $config;
30
	/** @var \App\Integrations\Comarch Controller instance. */
31
	public $controller;
32
	/** @var array Last scan config data. */
33
	protected $lastScan = [];
34
	/** @var int[] Imported ids */
35
	protected $imported = [];
36
	/** @var int[] Exported ids */
37
	protected $exported = [];
38
	/** @var string Category name used for the log mechanism */
39
	const LOG_CATEGORY = 'Integrations/Comarch';
40
	/** @var string The name of the configuration parameter for rows limit */
41
	const LIMIT_NAME = '';
42
	/** @var int Synchronization direction: one-way from Comarch to YetiForce */
43
	const DIRECTION_API_TO_YF = 0;
44
	/** @var int Synchronization direction: one-way from YetiForce to Comarch */
45
	const DIRECTION_YF_TO_API = 1;
46
	/** @var int Synchronization direction: two-way */
47
	const DIRECTION_TWO_WAY = 2;
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 = $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 = $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->controller->log('GetApiId', ['comarch_id' => $yfId, 'moduleName' => $moduleName], $th);
211
			\App\Log::error('Error GetApiId: ' . PHP_EOL . $th->__toString(), $this::LOG_CATEGORY);
212
		}
213
		$this->updateMapIdCache($moduleName, $apiId, $yfId);
214
		return $apiId;
215
	}
216
217
	/**
218
	 * Get YF value by API value.
219
	 *
220
	 * @param mixed $apiValue
221
	 * @param array $field
222
	 *
223
	 * @return mixed
224
	 */
225
	public function getYfValue($apiValue, 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

225
	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...
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

225
	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...
226
	{
227
		return '';
228
	}
229
230
	/**
231
	 * Get YF value by API value.
232
	 *
233
	 * @param mixed $yfValue
234
	 * @param array $field
235
	 *
236
	 * @return mixed
237
	 */
238
	public function getApiValue($yfValue, array $field)
0 ignored issues
show
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

238
	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...
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

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