Passed
Push — developer ( 5caef9...3bfca3 )
by Mariusz
17:23
created

Base::findByRelationship()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 3
1
<?php
2
3
/**
4
 * WooCommerce abstract base method synchronization file.
5
 *
6
 * The file is part of the paid functionality. Using the file is allowed only after purchasing a subscription.
7
 * File modification allowed only with the consent of the system producer.
8
 *
9
 * @package Integration
10
 *
11
 * @copyright YetiForce S.A.
12
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
13
 * @author    Mariusz Krzaczkowski <[email protected]>
14
 */
15
16
namespace App\Integrations\WooCommerce\Synchronizer\Maps;
17
18
/**
19
 * WooCommerce abstract base method synchronization class.
20
 */
21
abstract class Base
22
{
23
	/** @var string Map module name. */
24
	protected $moduleName;
25
	/** @var array Mapped fields. */
26
	protected $fieldMap = [];
27
	/** @var array Data from WooCommerce. */
28
	protected $dataApi = [];
29
	/** @var array Default data from WooCommerce. */
30
	protected $defaultDataApi = [];
31
	/** @var array Data from YetiForce. */
32
	protected $dataYf = [];
33
	/** @var array Default data from YetiForce. */
34
	protected $defaultDataYf = [];
35
	/** @var \App\Integrations\WooCommerce\Synchronizer\Base Synchronizer instance */
36
	protected $synchronizer;
37
	/** @var \Vtiger_Module_Model Module model instance */
38
	protected $moduleModel;
39
	/** @var \Vtiger_Record_Model Record model instance */
40
	protected $recordModel;
41
42
	/** @var string[] Mapped address fields. */
43
	protected $addressMapFields = [
44
		'addresslevel1' => ['name' => 'country', 'fn' => 'convertCountry'],
45
		'addresslevel2' => 'state',
46
		'addresslevel5' => 'city',
47
		'addresslevel7' => 'postcode',
48
		'addresslevel8' => 'address_1',
49
		'buildingnumber' => 'address_2',
50
		'first_name_' => 'first_name',
51
		'last_name_' => 'last_name',
52
		'phone_' => ['name' => 'phone', 'fn' => 'convertPhone'],
53
		'email_' => 'email',
54
		'company_name_' => 'company',
55
	];
56
57
	/**
58
	 * Constructor.
59
	 *
60
	 * @param \App\Integrations\WooCommerce\Synchronizer\Base $synchronizer
61
	 */
62
	public function __construct(\App\Integrations\WooCommerce\Synchronizer\Base $synchronizer)
63
	{
64
		$this->synchronizer = $synchronizer;
65
		$this->moduleModel = \Vtiger_Module_Model::getInstance($this->moduleName);
66
	}
67
68
	/**
69
	 * Set data from/for API.
70
	 *
71
	 * @param array $data
72
	 */
73
	public function setDataApi(array $data): void
74
	{
75
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
76
			$this->dataApi = $data;
77
		} else {
78
			$this->dataApi = $this->defaultDataApi;
79
		}
80
	}
81
82
	/**
83
	 * Set data from/for YetiForce. YetiForce data is read-only.
84
	 *
85
	 * @param array $data
86
	 * @param bool  $updateRecordModel
87
	 *
88
	 * @return void
89
	 */
90
	public function setDataYf(array $data, bool $updateRecordModel = false): void
91
	{
92
		$this->dataYf = $data;
93
		if ($updateRecordModel) {
94
			$this->recordModel = \Vtiger_Module_Model::getInstance($this->moduleName)->getRecordFromArray($data);
95
		}
96
	}
97
98
	/**
99
	 * Set data from/for YetiForce by record ID. Read/Write YetiForce data.
100
	 *
101
	 * @param int $id
102
	 *
103
	 * @return void
104
	 */
105
	public function setDataYfById(int $id): void
106
	{
107
		$this->recordModel = \Vtiger_Record_Model::getInstanceById($id, $this->moduleName);
108
		$this->dataYf = $this->recordModel->getData();
109
	}
110
111
	/**
112
	 * Load record model.
113
	 *
114
	 * @param int $id
115
	 */
116
	public function loadRecordModel(int $id): void
117
	{
118
		if ($id) {
119
			$this->recordModel = \Vtiger_Record_Model::getInstanceById($id, $this->moduleName);
120
		} else {
121
			$this->recordModel = \Vtiger_Record_Model::getCleanInstance($this->moduleName);
122
		}
123
	}
124
125
	/**
126
	 * Get record model.
127
	 *
128
	 * @return \Vtiger_Record_Model
129
	 */
130
	public function getRecordModel(): \Vtiger_Record_Model
131
	{
132
		return $this->recordModel;
133
	}
134
135
	/**
136
	 * Get module name.
137
	 *
138
	 * @return string
139
	 */
140
	public function getModule(): string
141
	{
142
		return $this->moduleName;
143
	}
144
145
	/**
146
	 * Return fields list.
147
	 *
148
	 * @return array
149
	 */
150
	public function getFields(): array
151
	{
152
		return $this->fieldMap;
153
	}
154
155
	/**
156
	 * Return parsed data in YetiForce format.
157
	 *
158
	 * @param string $type
159
	 * @param bool   $mapped
160
	 *
161
	 * @return array
162
	 */
163
	public function getDataYf(string $type = 'fieldMap', bool $mapped = true): array
164
	{
165
		if ($mapped) {
166
			$this->dataYf = $this->defaultDataYf[$type] ?? [];
167
			$this->parseMetaDataFromApi();
168
			foreach ($this->{$type} as $fieldCrm => $field) {
169
				if (\is_array($field)) {
170
					if (!empty($field['direction']) && 'api' === $field['direction']) {
171
						continue;
172
					}
173
					$field['fieldCrm'] = $fieldCrm;
174
175
					if (\is_array($field['name'])) {
176
						$this->loadDataYfMultidimensional($fieldCrm, $field);
177
					} elseif (\array_key_exists($field['name'], $this->dataApi)) {
178
						$this->loadDataYfMap($fieldCrm, $field);
179
					} elseif (!\array_key_exists('optional', $field) || empty($field['optional'])) {
180
						$error = "[API>YF][1] No column {$field['name']} ($fieldCrm)";
181
						\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
182
						$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataApi], null, true);
183
					}
184
				} else {
185
					$this->dataYf[$fieldCrm] = $this->dataApi[$field] ?? null;
186
					if (!\array_key_exists($field, $this->dataApi)) {
187
						$error = "[API>YF][2] No column $field ($fieldCrm)";
188
						\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
189
						$this->synchronizer->log($error, $this->dataApi, null, true);
190
					}
191
				}
192
			}
193
		}
194
		return $this->dataYf;
195
	}
196
197
	/**
198
	 * Parse data to YetiForce format from multidimensional array.
199
	 *
200
	 * @param string $fieldCrm
201
	 * @param array  $field
202
	 *
203
	 * @return void
204
	 */
205
	protected function loadDataYfMultidimensional(string $fieldCrm, array $field): void
206
	{
207
		$value = $this->dataApi;
208
		$field['fieldCrm'] = $fieldCrm;
209
		foreach ($field['name'] as $name) {
210
			if (\array_key_exists($name, $value)) {
211
				$value = $value[$name];
212
			} else {
213
				$error = "[API>YF][3] No column $name ($fieldCrm)";
214
				if (!\array_key_exists('optional', $field) || empty($field['optional'])) {
215
					\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
216
					$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataApi], null, true);
217
				}
218
			}
219
		}
220
		if (empty($error)) {
221
			$this->loadDataYfMap($fieldCrm, $field, $value);
222
		}
223
	}
224
225
	/**
226
	 * Parse data to YetiForce format from map.
227
	 *
228
	 * @param string     $fieldCrm
229
	 * @param array      $field
230
	 * @param mixed|null $value
231
	 *
232
	 * @return void
233
	 */
234
	protected function loadDataYfMap(string $fieldCrm, array $field, $value = null): void
235
	{
236
		$value = $value ?? $this->dataApi[$field['name']];
237
		if (isset($field['map'])) {
238
			if (\array_key_exists($value, $field['map'])) {
239
				$this->dataYf[$fieldCrm] = $field['map'][$value];
240
			} elseif (empty($field['mayNotExist'])) {
241
				$value = print_r($value, true);
242
				$error = "[API>YF] No value `{$value}` in map {$field['name']}";
243
				\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
244
				$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataApi], null, true);
245
			}
246
		} elseif (isset($field['fn'])) {
247
			$this->dataYf[$fieldCrm] = $this->{$field['fn']}($value, $field, true);
248
		} else {
249
			$this->dataYf[$fieldCrm] = $value;
250
		}
251
	}
252
253
	/**
254
	 * Create/update product in YF.
255
	 *
256
	 * @return void
257
	 */
258
	public function saveInYf(): void
259
	{
260
		foreach ($this->dataYf as $key => $value) {
261
			$this->recordModel->set($key, $value);
262
		}
263
		if ($this->recordModel->isEmpty('assigned_user_id')) {
264
			$this->recordModel->set('assigned_user_id', $this->synchronizer->config->get('assigned_user_id'));
265
		}
266
		if (
267
			$this->recordModel->isEmpty('woocommerce_id')
268
			&& $this->recordModel->getModule()->getFieldByName('woocommerce_id')
269
			&& !empty($this->dataApi['id'])
270
		) {
271
			$this->recordModel->set('woocommerce_id', $this->dataApi['id']);
272
		}
273
		$this->recordModel->set('woocommerce_server_id', $this->synchronizer->config->get('id'));
274
		$isNew = empty($this->recordModel->getId());
275
		$this->recordModel->save();
276
		$this->recordModel->ext['isNew'] = $isNew;
277
		if ($isNew && $this->recordModel->get('woocommerce_id')) {
278
			$this->synchronizer->updateMapIdCache(
279
				$this->recordModel->getModuleName(),
280
				$this->recordModel->get('woocommerce_id'), $this->recordModel->getId()
281
			);
282
		}
283
	}
284
285
	/**
286
	 * Return parsed data in YetiForce format.
287
	 *
288
	 * @param bool $mapped
289
	 *
290
	 * @return array
291
	 */
292
	public function getDataApi(bool $mapped = true): array
293
	{
294
		if ($mapped) {
295
			if (!empty($this->dataYf['woocommerce_id'])) {
296
				$this->dataApi['id'] = $this->dataYf['woocommerce_id'];
297
			}
298
			foreach ($this->fieldMap as $fieldCrm => $field) {
299
				if (\is_array($field)) {
300
					if (!empty($field['direction']) && 'yf' === $field['direction']) {
301
						continue;
302
					}
303
					if (\array_key_exists($fieldCrm, $this->dataYf)) {
304
						$field['fieldCrm'] = $fieldCrm;
305
						if (isset($field['map'])) {
306
							$mapValue = array_search($this->dataYf[$fieldCrm], $field['map']);
307
							if (false !== $mapValue) {
308
								$this->setApiData($mapValue, $field);
309
							} elseif (empty($field['mayNotExist'])) {
310
								$error = "[YF>API] No value `{$this->dataYf[$fieldCrm]}` in map {$fieldCrm}";
311
								\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
312
								$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataYf], null, true);
313
							}
314
						} elseif (isset($field['fn'])) {
315
							$this->setApiData($this->{$field['fn']}($this->dataYf[$fieldCrm], $field, false), $field);
316
						} else {
317
							$this->setApiData($this->dataYf[$fieldCrm], $field);
318
						}
319
					} elseif (!\array_key_exists('optional', $field) || empty($field['optional'])) {
320
						$error = '[YF>API] No field ' . $fieldCrm;
321
						\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
322
						$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataYf], null, true);
323
					}
324
				} else {
325
					$this->dataApi[$field] = $this->dataYf[$fieldCrm] ?? null;
326
					if (!\array_key_exists($fieldCrm, $this->dataYf)) {
327
						$error = '[YF>API] No field ' . $fieldCrm;
328
						\App\Log::warning($error, $this->synchronizer::LOG_CATEGORY);
329
						$this->synchronizer->log($error, ['fieldConfig' => $field, 'data' => $this->dataYf], null, true);
330
					}
331
				}
332
			}
333
			$this->parseMetaDataToApi();
334
		}
335
		return $this->dataApi;
336
	}
337
338
	/**
339
	 * Set the data to in the appropriate key structure.
340
	 *
341
	 * @param mixed $value
342
	 * @param array $field
343
	 *
344
	 * @return void
345
	 */
346
	public function setApiData($value, array $field): void
347
	{
348
		if (\is_array($field['name'])) {
349
			foreach (array_reverse($field['name']) as $name) {
350
				$value = [$name => $value];
351
			}
352
			$this->dataApi = \App\Utils::merge($this->dataApi, $value);
353
		} else {
354
			$this->dataApi[$field['name']] = $value;
355
		}
356
	}
357
358
	/**
359
	 * Create/update product by API.
360
	 *
361
	 * @return void
362
	 */
363
	public function saveInApi(): void
364
	{
365
		throw new \App\Exceptions\AppException('Method not implemented');
366
	}
367
368
	/**
369
	 * Convert bool to system format.
370
	 *
371
	 * @param mixed $value
372
	 * @param array $field
373
	 * @param bool  $fromApi
374
	 *
375
	 * @return int|bool int (YF) or bool (API)
376
	 */
377
	protected function convertBool($value, array $field, bool $fromApi)
378
	{
379
		if ($fromApi) {
380
			return $value ? 1 : 0;
381
		}
382
		return 1 == $value;
383
	}
384
385
	/**
386
	 * Convert date time to system format.
387
	 *
388
	 * @param mixed $value
389
	 * @param array $field
390
	 * @param bool  $fromApi
391
	 *
392
	 * @return string Date time Y-m-d H:i:s (YF) or Y-m-d\TH:i:s (API)
393
	 */
394
	protected function convertDateTime($value, array $field, bool $fromApi): string
395
	{
396
		if ($fromApi) {
397
			return \DateTimeField::convertTimeZone($value, 'UTC', \App\Fields\DateTime::getTimeZone())
398
				->format('Y-m-d H:i:s');
399
		}
400
		return \DateTimeField::convertTimeZone($value, \App\Fields\DateTime::getTimeZone(), 'UTC')
401
			->format('Y-m-d\TH:i:s');
402
	}
403
404
	/**
405
	 * Convert date to system format.
406
	 *
407
	 * @param mixed $value
408
	 * @param array $field
409
	 * @param bool  $fromApi
410
	 *
411
	 * @return string
412
	 */
413
	protected function convertDate($value, array $field, bool $fromApi)
414
	{
415
		return date('Y-m-d', strtotime($value));
416
	}
417
418
	/**
419
	 * Convert price to system format.
420
	 *
421
	 * @param array $field
422
	 * @param mixed $value
423
	 * @param bool  $fromApi
424
	 *
425
	 * @return string|float JSON (YF) or string (API)
426
	 */
427
	protected function convertPrice($value, array $field, bool $fromApi)
428
	{
429
		$currency = $this->synchronizer->config->get('currency_id');
430
		if ($fromApi) {
431
			return \App\Json::encode([
432
				'currencies' => [
433
					$currency => ['price' => $value ?: 0]
434
				],
435
				'currencyId' => $currency
436
			]);
437
		}
438
		$value = \App\Json::decode($value);
439
		return (string) (empty($value['currencies'][$currency]) ? 0 : $value['currencies'][$currency]['price']);
440
	}
441
442
	/**
443
	 * Convert price to system format.
444
	 *
445
	 * @param mixed $value
446
	 * @param array $field
447
	 * @param bool  $fromApi
448
	 *
449
	 * @return mixed
450
	 */
451
	protected function convert($value, array $field, bool $fromApi)
452
	{
453
		switch ($field[$fromApi ? 'crmType' : 'apiType'] ?? 'string') {
454
			default:
455
			case 'string':
456
				$value = (string) $value;
457
				break;
458
			case 'float':
459
				$value = (float) $value;
460
				break;
461
		}
462
		return $value;
463
	}
464
465
	/**
466
	 * Find relationship in YF by API ID.
467
	 *
468
	 * @param mixed $value
469
	 * @param array $field
470
	 * @param bool  $fromApi
471
	 *
472
	 * @return int
473
	 */
474
	protected function findByRelationship($value, array $field, bool $fromApi): int
475
	{
476
		$moduleName = $field['moduleName'] ?? $this->moduleName;
477
		if (empty($value)) {
478
			return 0;
479
		}
480
		if ($fromApi) {
481
			return $this->synchronizer->getYfId($value, $moduleName);
482
		}
483
		return $this->synchronizer->getApiId($value, $moduleName);
484
	}
485
486
	/**
487
	 * Add relationship in YF by API ID.
488
	 *
489
	 * @param mixed $value
490
	 * @param array $field
491
	 * @param bool  $fromApi
492
	 *
493
	 * @return string|array string (YF) or string (API)
494
	 */
495
	protected function addRelationship($value, array $field, bool $fromApi)
496
	{
497
		$moduleName = rtrim($field['moduleName'], 's');
498
		$key = mb_strtolower($moduleName);
499
		if (null === $this->{$key}) {
500
			$this->{$key} = $this->synchronizer->getMapModel($moduleName);
501
		}
502
		$this->{$key}->setDataApi($this->dataApi);
503
		return $this->{$key}->saveFromRelation($field);
504
	}
505
506
	/**
507
	 * Convert phone number to system YF format.
508
	 *
509
	 * @param mixed $value
510
	 * @param array $field
511
	 * @param bool  $fromApi
512
	 *
513
	 * @return string
514
	 */
515
	protected function convertPhone($value, array $field, bool $fromApi)
516
	{
517
		if (empty($value) || !$fromApi) {
518
			return $value;
519
		}
520
		$fieldCrm = $field['fieldCrm'];
521
		$parsedData = [$fieldCrm => $value];
522
		$parsedData = \App\Fields\Phone::parsePhone($fieldCrm, $parsedData);
523
		if (empty($parsedData[$fieldCrm])) {
524
			foreach ($parsedData as $key => $value) {
0 ignored issues
show
introduced by
$value is overwriting one of the parameters of this function.
Loading history...
525
				$this->dataYf[$key] = $value;
526
			}
527
			return '';
528
		}
529
		return $parsedData[$fieldCrm];
530
	}
531
532
	/**
533
	 * Convert country to system format.
534
	 *
535
	 * @param mixed $value
536
	 * @param array $field
537
	 * @param bool  $fromApi
538
	 *
539
	 * @return string|null Country name (YF) or Country code (API)
540
	 */
541
	protected function convertCountry($value, array $field, bool $fromApi)
542
	{
543
		if (empty($value)) {
544
			return $value;
545
		}
546
		return $fromApi ? \App\Fields\Country::getCountryName($value) : \App\Fields\Country::getCountryCode($value);
547
	}
548
549
	/**
550
	 * Convert address fields.
551
	 *
552
	 * @param string $source
553
	 * @param string $target
554
	 * @param bool   $checkField
555
	 *
556
	 * @return void
557
	 */
558
	protected function convertAddress(string $source, string $target, bool $checkField = true): void
559
	{
560
		foreach ($this->addressMapFields as $yf => $api) {
561
			if ($checkField && !$this->moduleModel->getFieldByName($yf . $target)) {
562
				\App\Log::info(
563
					"The {$yf}{$target} field does not exist in the {$this->moduleName} module",
564
					$this->synchronizer::LOG_CATEGORY);
565
				continue;
566
			}
567
			if (\is_array($api)) {
568
				$api['name'] = [$source, $api['name']];
569
				$this->loadDataYfMultidimensional($yf . $target, $api);
570
			} elseif (\array_key_exists($api, $this->dataApi[$source])) {
571
				$this->dataYf[$yf . $target] = $this->dataApi[$source][$api];
572
			}
573
		}
574
	}
575
576
	/**
577
	 * Convert currency.
578
	 *
579
	 * @param mixed $value
580
	 * @param array $field
581
	 * @param bool  $fromApi
582
	 *
583
	 * @return int|string int (YF) or string (API)
584
	 */
585
	protected function convertCurrency($value, array $field, bool $fromApi)
586
	{
587
		if ($fromApi) {
588
			$currency = \App\Fields\Currency::getIdByCode($value);
589
			if (empty($currency)) {
590
				$currency = \App\Fields\Currency::addCurrency($value);
591
			}
592
		} else {
593
			$currency = \App\Fields\Currency::getById($value)['currency_code'];
594
		}
595
		return $currency;
596
	}
597
598
	/**
599
	 * Save record in YF from relation action.
600
	 *
601
	 * @param array $field
602
	 *
603
	 * @return int
604
	 */
605
	public function saveFromRelation(array $field): int
606
	{
607
		$id = 0;
608
		if ($dataYf = $this->getDataYf()) {
609
			try {
610
				$id = $this->findRecordInYf();
611
				if (empty($field['onlyCreate']) || empty($id)) {
612
					$this->loadRecordModel($id);
613
					$this->loadAdditionalData();
614
					$this->saveInYf();
615
					$id = $this->getRecordModel()->getId();
616
				}
617
			} catch (\Throwable $ex) {
618
				$error = "[API>YF] Import {$this->moduleName}";
619
				\App\Log::warning($error . "\n" . $ex->getMessage(), $this->synchronizer::LOG_CATEGORY);
620
				$this->synchronizer->log($error, ['YF' => $dataYf, 'API' => $this->dataApi], $ex);
621
			}
622
		}
623
		return $id;
624
	}
625
626
	/**
627
	 * Find record in YetiFoce.
628
	 *
629
	 * @return int
630
	 */
631
	protected function findRecordInYf(): int
632
	{
633
		return $this->synchronizer->getYfId($this->dataApi['id'], $this->moduleName);
634
	}
635
636
	/**
637
	 * Parsing `meta_data` from API.
638
	 *
639
	 * @return void
640
	 */
641
	protected function parseMetaDataFromApi(): void
642
	{
643
		if ($this->dataApi['meta_data']) {
644
			$this->dataApi['metaData'] = [];
645
			foreach ($this->dataApi['meta_data'] as $value) {
646
				$this->dataApi['metaData'][$value['key']] = $value['value'];
647
			}
648
		}
649
	}
650
651
	/**
652
	 * Parsing `meta_data` to API.
653
	 *
654
	 * @return void
655
	 */
656
	protected function parseMetaDataToApi(): void
657
	{
658
		if ($this->dataApi['metaData']) {
659
			$this->dataApi['meta_data'] = [];
660
			foreach ($this->dataApi['metaData'] as $key => $value) {
661
				$this->dataApi['meta_data'][] = ['key' => $key, 'value' => $value];
662
			}
663
			unset($this->dataApi['metaData']);
664
		}
665
	}
666
667
	/**
668
	 * Load additional data.
669
	 *
670
	 * @return void
671
	 */
672
	public function loadAdditionalData(): void
673
	{
674
	}
675
}
676