EntityManager::createReferenceData()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 12

Duplication

Lines 7
Ratio 33.33 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 2
dl 7
loc 21
rs 9.3142
c 0
b 0
f 0
1
<?php
2
3
namespace DigitalWand\AdminHelper;
4
5
use Bitrix\Main\Localization\Loc;
6
use Bitrix\Main\ArgumentException;
7
use Bitrix\Main\Entity\DataManager;
8
use Bitrix\Main\Entity;
9
use DigitalWand\AdminHelper\Helper\AdminBaseHelper;
10
use DigitalWand\AdminHelper\Widget\HelperWidget;
11
12
/**
13
 * Менеджер для управления моделью, способный анализировать её связи и сохранять данные в связанные сущности на
14
 * основании полученных данных от виджетов.
15
 *
16
 * Пример создания сущности:
17
 * ```
18
 * $filmManager = new EntityManager('\Vendor\Module\FilmTable', array(
19
 *        // Данные сущности
20
 *        'TITLE' => 'Монстры на каникулах 2',
21
 *        'YEAR' => 2015,
22
 *        // У сущности FilmTable есть связь с RelatedLinksTable через поле RELATED_LINKS.
23
 *        // Если передать ей данные, то они будут обработаны
24
 *        // Представим, что у сущности RelatedLinksTable есть поля ID и VALUE (в этом поле хранится ссылка), FILM_ID
25
 *        // В большинстве случаев, данные передаваемые связям генерируются множественными виджетами
26
 *        'RELATED_LINKS' => array(
27
 *            // Переданный ниже массив будет обработан аналогично коду RelatedLinksTable::add(array('VALUE' =>
28
 * 'yandex.ru')); array('VALUE' => 'yandex.ru'),
29
 *            // Если в массив добавить ID, то запись обновится: RelatedLinksTable::update(3, array('ID' => 3, 'VALUE'
30
 * => 'google.com')); array('ID' => 3, 'VALUE' => 'google.com'),
31
 *            // ВНИМАНИЕ: данный класс реководствуется принципом: что передано для связи, то сохранится или обновится,
32
 * что не передано, будет удалено
33
 *            // То есть, если в поле связи RELATED_LINKS передать пустой массив, то все значения связи будут удалены
34
 *        )
35
 * ));
36
 * $filmManager->save();
37
 * ```
38
 *
39
 * Пример удаления сущности
40
 * ```
41
 * $articleManager = new EntityManager('\Vendor\Module\ArticlesTable', array(), 7, $adminHelper);
42
 * $articleManager->delete();
43
 * ```
44
 *
45
 * Как работает сохранение данных ? Дополнительный пример
46
 * Допустим, что есть модели NewsTable (новости) и NewsLinksTable (ссылки на дополнительную информацию о новости)
47
 *
48
 * У модели NewsTable есть связь с моделью NewsLinksTable через поле NEWS_LINKS:
49
 * ```
50
 * DataManager::getMap() {
51
 * ...
52
 * new Entity\ReferenceField(
53
 *        'NEWS_LINKS',
54
 *        '\Vendor\Module\NewsLinksTable',
55
 *        array('=this.ID' => 'ref.NEWS_ID'),
56
 *        'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'),
57
 *        'ref.ENTITY' => new DB\SqlExpression('?s', 'news'),
58
 * ),
59
 * ...
60
 * }
61
 * ```
62
 *
63
 * Попробуем сохранить
64
 * ```
65
 * $newsManager = new EntityManager(
66
 *        '\Vendor\Module\NewsTable',
67
 *        array(
68
 *            'TITLE' => 'News title',
69
 *            'CONTENT' => 'News content',
70
 *            'NEWS_LINKS' => array(
71
 *                array('LINK' => 'test.ru'),
72
 *                array('LINK' => 'test2.ru'),
73
 *                array('ID' => 'id ссылки', 'LINK' => 'test3.ru'),
74
 *            )
75
 *        ),
76
 *        null,
77
 *        $adminHelper
78
 * );
79
 * $newsManager->save();
80
 * ```
81
 *
82
 * В данном примере передаются данные для новости (заголовок и содержимое) и данные для поля-связи NEWS_LINKS.
83
 *
84
 * Далее EntityManager:
85
 * 1. Вырезает данные, которые предназначены связям
86
 * 2. Подставляет в них данные из основной модели на основе условий связи
87
 * Например для связи с полем NEWS_LINKS подставятся данные:
88
 *
89
 * ```
90
 * NewsLinksTable::ENTITY_ID => NewsTable::ID,
91
 * NewsLinksTable::FIELD => 'NEWS_LINKS',
92
 * NewsLinksTable::ENTITY => 'news'
93
 * ```
94
 *
95
 * 3. После подстановки данных они будут переданы модели связи подобно коду ниже:
96
 *
97
 * ```
98
 * NewsLinksTable::add(array('ENTITY' => 'news', 'FIELD' => 'NEWS_LINKS', 'ENTITY_ID' => 'id сущности, например
99
 * новости', 'LINK' => 'test.ru')); NewsLinksTable::add(array('ENTITY' => 'news', 'FIELD' => 'NEWS_LINKS', 'ENTITY_ID'
100
 * => 'id сущности', 'LINK' => 'test2.ru')); NewsLinksTable::update('id ссылки', array('ENTITY' => 'news', 'FIELD' =>
101
 * 'NEWS_LINKS', 'ENTITY_ID' => 'id сущности', 'LINK' => 'test3.ru'));
102
 * ```
103
 *
104
 * Обратите внимание, что в метод EntityManager::save() были изначально передано только поле LINK, поля ENTITY,
105
 * ENTITY_ID и FIELD были подставлены классом EntityManager автоматически (предыдущий пункт) А так же важно, что для
106
 * третьей ссылки был передан идентификатор, поэтому выполнился NewsLinksTable::update, а не NewsLinksTable::add
107
 *
108
 * 4. Далее `EntityManager` удаляет данные связанной модели `NewsLinksTable`, которые не были добавлены или обновлены.
109
 *
110
 * <b>Как работает удаление?</b>
111
 *
112
 * 1. EntityManager получает из `NewsTable::getMap()` поля-связи
113
 * 2. Получает поля описанные в интерфейсе генератора админки
114
 * 3. Удаляет значения для полей-связей, которые объявлены в интерфейсе
115
 *
116
 * <i>Примечание.</i>
117
 * EntityManager управляет только данными, которые получает при помощи связи стандартными средставами битрикса.
118
 * Например, при удалении NewsTable будут удалены только NewsLinksTable, где:
119
 *
120
 * ```
121
 * NewsLinksTable::ENTITY_ID => NewsTable::ID,
122
 * NewsLinksTable::FIELD => 'NEWS_LINKS',
123
 * NewsLinksTable::ENTITY => 'news'
124
 * ```
125
 *
126
 * @author Nik Samokhvalov <[email protected]>
127
 * @author Dmitriy Baibuhtin <[email protected]>
128
 */
129
class EntityManager
130
{
131
	/**
132
	 * @var string Класс модели.
133
	 */
134
	protected $modelClass;
135
	/**
136
	 * @var Entity\Base Сущность модели.
137
	 */
138
	protected $model;
139
	/**
140
	 * @var array Данные для обработки.
141
	 */
142
	protected $data;
143
	/**
144
	 * @var integer Идентификатор записи.
145
	 */
146
	protected $itemId = null;
147
	/**
148
	 * @var string Поле модели, в котором хранится идентификатор записи.
149
	 */
150
	protected $modelPk = null;
151
	/**
152
	 * @var array Данные для связей.
153
	 */
154
	protected $referencesData = array();
155
	/**
156
	 * @var AdminBaseHelper Хелпер.
157
	 */
158
	protected $helper;
159
	/**
160
	 * @var array Предупреждения.
161
	 */
162
	protected $notes = array();
163
164
	/**
165
	 * @param string $modelClass Класс основной модели, наследника DataManager.
166
	 * @param array $data Массив с сохраняемыми данными.
167
	 * @param integer $itemId Идентификатор сохраняемой записи.
168
	 * @param AdminBaseHelper $helper Хелпер, инициирующий сохранение записи.
169
	 */
170
	public function __construct($modelClass, array $data = array(), $itemId = null, AdminBaseHelper $helper)
171
	{
172
		Loc::loadMessages(__FILE__);
173
		
174
		$this->modelClass = $modelClass;
175
		$this->model = $modelClass::getEntity();
176
		$this->data = $data;
177
		$this->modelPk = $this->model->getPrimary();
178
		$this->helper = $helper;
179
180
		if (!empty($itemId)) {
181
			$this->setItemId($itemId);
182
		}
183
	}
184
185
    /**
186
     * Сохранить запись и данные связей.
187
     *
188
     * @return Entity\AddResult|Entity\UpdateResult
189
     */
190
    public function save()
191
    {
192
        $this->collectReferencesData();
193
194
        /**
195
         * @var DataManager $modelClass
196
         */
197
        $modelClass = $this->modelClass;
198
		$db = $this->model->getConnection();
199
		$db->startTransaction(); // начало транзакции
200
201
		if (empty($this->itemId)) {
202
			$result = $modelClass::add($this->data);
203
204
			if ($result->isSuccess()) {
205
				$this->setItemId($result->getId());
206
			}
207
		}
208
		else {
209
			$result = $modelClass::update($this->itemId, $this->data);
210
		}
211
212
        if ($result->isSuccess()) {
213
			$referencesDataResult = $this->processReferencesData();
214
			if($referencesDataResult->isSuccess()){
215
				$db->commitTransaction(); // ошибок нет - применяем изменения
216
			}else{
217
				$result = $referencesDataResult; // возвращаем ReferencesResult что бы вернуть ошибку
218
				$db->rollbackTransaction(); // что-то пошло не так - возвращаем все как было
219
			}
220
		} else {
221
			$db->rollbackTransaction();
222
		}
223
224
		return $result;
225
	}
226
227
    /**
228
     * Удаление запись и данные связей.
229
     *
230
     * @return Entity\DeleteResult
231
     */
232
    public function delete()
233
    {
234
        // Удаление данных зависимостей
235
		$db = $this->model->getConnection();
236
		$db->startTransaction(); // начало транзакции
237
238
		$result = $this->deleteReferencesData(); // удаляем зависимые сущности
239
240
		if(!$result->isSuccess()){ // если хотя бы одна из них не удалилась
241
			$db->rollbackTransaction(); // то восстанавливаем все
242
			return $result; // возвращаем ошибку
243
		}
244
245
		$model = $this->modelClass;
246
247
		//Если передается массив, то получаем ИД записи
248
        if (!is_null($this->itemId)) {
249
            $result = $model::delete($this->itemId); // удаляем основную сущность
250
        } elseif (!is_array($this->helper->getPk())) {
251
            $result = $model::delete($this->helper->getPk()); // удаляем основную сущность
252
        } else {
253
            $result = new Entity\DeleteResult();
254
            $error = new Entity\EntityError('Can\'t find element\'s ID');
255
            $result->addError($error);
256
        }
257
258
		if(!$result->isSuccess()){  // если не удалилась
259
			$db->rollbackTransaction(); // то восстанавливаем зависимые сущности
260
			return $result; // возвращаем ошибку
261
		}
262
263
		$db->commitTransaction(); // все прошло без ошибок применяем изменения
264
		return $result;
265
    }
266
267
	/**
268
	 * Получить список предупреждений
269
	 * @return array
270
	 */
271
	public function getNotes()
272
	{
273
		return $this->notes;
274
	}
275
276
	/**
277
	 * Добавить предупреждение
278
	 *
279
	 * @param $note
280
	 * @param string $key Ключ для избежания дублирования сообщений
281
	 *
282
	 * @return bool
283
	 */
284
	protected function addNote($note, $key = null)
285
	{
286
		if ($key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
287
			$this->notes[$key] = $note;
288
		}
289
		else {
290
			$this->notes[] = $note;
291
		}
292
293
		return true;
294
	}
295
296
	/**
297
	 * Установка текущего идентификатора модели.
298
	 *
299
	 * @param integer $itemId Идентификатор записи.
300
	 */
301
	protected function setItemId($itemId)
302
	{
303
		$this->itemId = $itemId;
304
		$this->data[$this->modelPk] = $this->itemId;
305
	}
306
307
	/**
308
	 * Получение связей
309
	 *
310
	 * @return array
311
	 */
312
	protected function getReferences()
313
	{
314
		/**
315
		 * @var DataManager $modelClass
316
		 */
317
		$modelClass = $this->modelClass;
318
		$entity = $modelClass::getEntity();
319
		$fields = $entity->getFields();
320
		$references = array();
321
322
		foreach ($fields as $fieldName => $field) {
323
			if ($field instanceof Entity\ReferenceField) {
324
				$references[$fieldName] = $field;
325
			}
326
		}
327
328
		return $references;
329
	}
330
331
	/**
332
	 * Извлечение данных для связей
333
	 */
334
	protected function collectReferencesData()
335
	{
336
		$result = new Entity\Result();
337
		$references = $this->getReferences();
338
		// Извлечение данных управляемых связей
339
		foreach ($references as $fieldName => $reference) {
340
			if (array_key_exists($fieldName, $this->data)) {
341
				if (!is_array($this->data[$fieldName])) {
342
					$result->addError(new Entity\EntityError(Loc::getMessage('DIGITALWAND_AH_RELATION_SHOULD_BE_MULTIPLE_FIELD')));
343
344
					return $result;
345
				}
346
				// Извлечение данных для связи
347
				$this->referencesData[$fieldName] = $this->data[$fieldName];
348
				unset($this->data[$fieldName]);
349
			}
350
		}
351
352
		return $result;
353
	}
354
355
    /**
356
     * Обработка данных для связей.
357
     *
358
     * @throws ArgumentException
359
     */
360
    protected function processReferencesData()
361
    {
362
        /**
363
         * @var DataManager $modelClass
364
         */
365
        $modelClass = $this->modelClass;
366
        $entity = $modelClass::getEntity();
367
        $fields = $entity->getFields();
368
		$result = new Entity\Result(); // пустой Result у которого isSuccess вернет true
369
370
		foreach ($this->referencesData as $fieldName => $referenceDataSet) {
371
			if (!is_array($referenceDataSet)) {
372
				continue;
373
			}
374
375
			/**
376
			 * @var Entity\ReferenceField $reference
377
			 */
378
			$reference = $fields[$fieldName];
379
			$referenceDataSet = $this->linkDataSet($reference, $referenceDataSet);
380
			$referenceStaleDataSet = $this->getReferenceDataSet($reference);
381
			$fieldWidget = $this->getFieldWidget($fieldName);
382
383
			// Создание и обновление привязанных данных
384
			$processedDataIds = array();
385
			foreach ($referenceDataSet as $referenceData) {
386
				if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
387
					// Создание связанных данных
388
					if (!empty($referenceData[$fieldWidget->getMultipleField('VALUE')])) {
389
						$result = $this->createReferenceData($reference, $referenceData);
390
391
                        if ($result->isSuccess()) {
392
                            $processedDataIds[] = $result->getId();
393
                        } else {
394
							break; // ошибка, прерываем обработку данных
395
						}
396
                    }
397
                } else {
398
                    // Обновление связанных данных
399
					$result = $this->updateReferenceData($reference, $referenceData, $referenceStaleDataSet);
400
401
                    if ($result->isSuccess()) {
402
                        $processedDataIds[] = $referenceData[$fieldWidget->getMultipleField('ID')];
403
                    } else {
404
						break; // ошибка, прерываем обработку данных
405
					}
406
                }
407
            }
408
409
			if($result->isSuccess()){ // Удаление записей, которые не были созданы или обновлены
410
				foreach ($referenceStaleDataSet as $referenceData) {
411
					if (!in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)) {
412
						$result = $this->deleteReferenceData($reference,
413
							$referenceData[$fieldWidget->getMultipleField('ID')]);
414
						if(!$result->isSuccess()) {
415
							break; // ошибка, прерываем удаление данных
416
						}
417
					}
418
				}
419
			}
420
        }
421
422
        $this->referencesData = array();
423
		return $result;
424
    }
425
426
    /**
427
     * Удаление данных всех связей, которые указаны в полях интерфейса раздела.
428
     */
429
    protected function deleteReferencesData()
430
    {
431
        $references = $this->getReferences();
432
        $fields = $this->helper->getFields();
433
		$result = new Entity\Result();
434
        /**
435
         * @var string $fieldName
436
         * @var Entity\ReferenceField $reference
437
         */
438
        foreach ($references as $fieldName => $reference) {
439
            // Удаляются только данные связей, которые объявлены в интерфейсе
440
            if (!isset($fields[$fieldName])) {
441
                continue;
442
            }
443
444
			$fieldWidget = $this->getFieldWidget($reference->getName());
445
			$referenceStaleDataSet = $this->getReferenceDataSet($reference);
446
447
            foreach ($referenceStaleDataSet as $referenceData) {
448
                $result = $this->deleteReferenceData($reference, $referenceData[$fieldWidget->getMultipleField('ID')]);
449
				if(!$result->isSuccess()){
450
					return $result;
451
				}
452
            }
453
        }
454
		return $result;
455
    }
456
457
	/**
458
	 * Создание связанной записи.
459
	 *
460
	 * @param Entity\ReferenceField $reference
461
	 * @param array $referenceData
462
	 *
463
	 * @return \Bitrix\Main\Entity\AddResult
464
	 * @throws ArgumentException
465
	 */
466
	protected function createReferenceData(Entity\ReferenceField $reference, array $referenceData)
467
	{
468
		$referenceName = $reference->getName();
469
		$fieldParams = $this->getFieldParams($referenceName);
470
		$fieldWidget = $this->getFieldWidget($referenceName);
471
472 View Code Duplication
		if (!empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
473
			throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_CANT_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
474
		}
475
476
		$refClass = $reference->getRefEntity()->getDataClass();
477
478
		$createResult = $refClass::add($referenceData);
479
480 View Code Duplication
		if (!$createResult->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
481
			$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
482
				array('#FIELD#' => $fieldParams['TITLE'])), 'CREATE_' . $referenceName);
483
		}
484
485
		return $createResult;
486
	}
487
488
	/**
489
	 * Обновление связанной записи
490
	 *
491
	 * @param Entity\ReferenceField $reference
492
	 * @param array $referenceData
493
	 * @param array $referenceStaleDataSet
494
	 *
495
	 * @return Entity\UpdateResult|null
496
	 * @throws ArgumentException
497
	 */
498
	protected function updateReferenceData(
499
		Entity\ReferenceField $reference,
500
		array $referenceData,
501
		array $referenceStaleDataSet
502
	)
503
	{
504
		$referenceName = $reference->getName();
505
		$fieldParams = $this->getFieldParams($referenceName);
506
		$fieldWidget = $this->getFieldWidget($referenceName);
507
508 View Code Duplication
		if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
509
			throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_SHOULD_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
510
		}
511
512
		// Сравнение старых данных и новых, обновляется только при различиях
513
		if ($this->isDifferentData($referenceStaleDataSet[$referenceData[$fieldWidget->getMultipleField('ID')]],
514
			$referenceData)
515
		) {
516
			$refClass = $reference->getRefEntity()->getDataClass();
517
			$updateResult = $refClass::update($referenceData[$fieldWidget->getMultipleField('ID')], $referenceData);
518
519 View Code Duplication
			if (!$updateResult->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
520
				$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
521
					array('#FIELD#' => $fieldParams['TITLE'])), 'UPDATE_' . $referenceName);
522
			}
523
524
            return $updateResult;
525
        } else {
526
            return new Entity\Result(); // пустой Result у которого isSuccess() вернет true
527
        }
528
    }
529
530
	/**
531
	 * Удаление данных связи.
532
	 *
533
	 * @param Entity\ReferenceField $reference
534
	 * @param $referenceId
535
	 *
536
	 * @return \Bitrix\Main\Entity\Result
537
	 * @throws ArgumentException
538
	 */
539
	protected function deleteReferenceData(Entity\ReferenceField $reference, $referenceId)
540
	{
541
		$fieldParams = $this->getFieldParams($reference->getName());
542
		$refClass = $reference->getRefEntity()->getDataClass();
543
		$deleteResult = $refClass::delete($referenceId);
544
545
		if (!$deleteResult->isSuccess()) {
546
			$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR',
547
				array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName());
548
		}
549
550
		return $deleteResult;
551
	}
552
553
	/**
554
	 * Получение данных связи.
555
	 *
556
	 * @param $reference
557
	 *
558
	 * @return array
559
	 */
560
	protected function getReferenceDataSet(Entity\ReferenceField $reference)
561
	{
562
		/**
563
		 * @var DataManager $modelClass
564
		 */
565
		$modelClass = $this->modelClass;
566
		$dataSet = array();
567
		$fieldWidget = $this->getFieldWidget($reference->getName());
568
569
		$rsData = $modelClass::getList(array(
570
			'select' => array('REF_' => $reference->getName() . '.*'),
571
			'filter' => array('=' . $this->modelPk => $this->itemId)
572
		));
573
574
		while ($data = $rsData->fetch()) {
575
			if (empty($data['REF_' . $fieldWidget->getMultipleField('ID')])) {
576
				continue;
577
			}
578
579
			$row = array();
580
			foreach ($data as $key => $value) {
581
				$row[str_replace('REF_', '', $key)] = $value;
582
			}
583
584
			$dataSet[$data['REF_' . $fieldWidget->getMultipleField('ID')]] = $row;
585
		}
586
587
		return $dataSet;
588
	}
589
590
	/**
591
	 * В данные связи подставляются данные основной модели используя условия связи моделей из getMap().
592
	 *
593
	 * @param Entity\ReferenceField $reference
594
	 * @param array $referenceData Данные привязанной модели
595
	 *
596
	 * @return array
597
	 */
598
	protected function linkData(Entity\ReferenceField $reference, array $referenceData)
599
	{
600
		// Парсим условия связи двух моделей
601
		$referenceConditions = $this->getReferenceConditions($reference);
602
603
		foreach ($referenceConditions as $refField => $refValue) {
604
			// Так как в условиях связи между моделями в основном отношения типа this.field => ref.field или
605
			// ref.field => SqlExpression, мы можем использовать это для подстановки данных
606
			// this.field - это поле основной модели
607
			// ref.field - поле модели из связи
608
			// customValue - это строка полученная из new SqlExpression('%s', ...)
609
			if (empty($refValue['thisField'])) {
610
				$referenceData[$refField] = $refValue['customValue'];
611
			}
612
			else {
613
				$referenceData[$refField] = $this->data[$refValue['thisField']];
614
			}
615
		}
616
617
		return $referenceData;
618
	}
619
620
	/**
621
	 * Связывает набор связанных данных с основной моделю.
622
	 *
623
	 * @param Entity\ReferenceField $reference
624
	 * @param array $referenceDataSet
625
	 *
626
	 * @return array
627
	 */
628
	protected function linkDataSet(Entity\ReferenceField $reference, array $referenceDataSet)
629
	{
630
		foreach ($referenceDataSet as $key => $referenceData) {
631
			$referenceDataSet[$key] = $this->linkData($reference, $referenceData);
632
		}
633
634
		return $referenceDataSet;
635
	}
636
637
	/**
638
	 * Парсинг условий связи между моделями.
639
	 *
640
	 * Ничего сложного нет, просто определяются соответствия полей основной модели и модели из связи. Например:
641
	 *
642
	 * `FilmLinksTable::FILM_ID => FilmTable::ID (ref.FILM_ID => this.ID)`
643
	 *
644
	 * Или, например:
645
	 *
646
	 * `MediaTable::TYPE => 'FILM' (ref.TYPE => new DB\SqlExpression('?s', 'FILM'))`
647
	 *
648
	 * @param Entity\ReferenceField $reference Данные поля из getMap().
649
	 *
650
	 * @return array Условия связи преобразованные в массив вида $conditions[$refField]['thisField' => $thisField,
651
	 *     'customValue' => $customValue].
652
	 *      $customValue - это результат парсинга SqlExpression.
653
	 *      Если шаблон SqlExpression не равен %s, то условие исключается из результата.
654
	 */
655
	protected function getReferenceConditions(Entity\ReferenceField $reference)
656
	{
657
		$conditionsFields = array();
658
659
		foreach ($reference->getReference() as $leftCondition => $rightCondition) {
660
			$thisField = null;
661
			$refField = null;
662
			$customValue = null;
663
664
			// Поиск this.... в левом условии
665
			$thisFieldMatch = array();
666
			$refFieldMatch = array();
667
			if (preg_match('/=this\.([A-z]+)/', $leftCondition, $thisFieldMatch) == 1) {
668
				$thisField = $thisFieldMatch[1];
669
			} // Поиск ref.... в левом условии
670
			else {
671
				if (preg_match('/ref\.([A-z]+)/', $leftCondition, $refFieldMatch) == 1) {
672
					$refField = $refFieldMatch[1];
673
				}
674
			}
675
676
			// Поиск expression value... в правом условии
677
			$refFieldMatch = array();
678
			if ($rightCondition instanceof \Bitrix\Main\DB\SqlExpression) {
679
				$customValueDirty = $rightCondition->compile();
680
				$customValue = preg_replace('/^([\'"])(.+)\1$/', '$2', $customValueDirty);
681
				if ($customValueDirty == $customValue) {
682
					// Если значение выражения не обрамлено кавычками, значит оно не нужно нам
683
					$customValue = null;
684
				}
685
			} // Поиск ref.... в правом условии
686
			else {
687
				if (preg_match('/ref\.([A-z]+)/', $rightCondition, $refFieldMatch) > 0) {
688
					$refField = $refFieldMatch[1];
689
				}
690
			}
691
692
			// Если не указано поле, которое нужно заполнить или не найдено содержимое для него, то исключаем условие
693
			if (empty($refField) || (empty($thisField) && empty($customValue))) {
694
				continue;
695
			}
696
			else {
697
				$conditionsFields[$refField] = array(
698
					'thisField' => $thisField,
699
					'customValue' => $customValue,
700
				);
701
			}
702
		}
703
704
		return $conditionsFields;
705
	}
706
707
	/**
708
	 * Обнаружение отличий массивов
709
	 * Метод не сранивает наличие аргументов, сравниваются только значения общих параметров
710
	 *
711
	 * @param array $data1
712
	 * @param array $data2
713
	 *
714
	 * @return bool
715
	 */
716
	protected function isDifferentData(array $data1 = null, array $data2 = null)
717
	{
718
		foreach ($data1 as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $data1 of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
719
			if (isset($data2[$key]) && $data2[$key] != $value) {
720
				return true;
721
			}
722
		}
723
724
		return false;
725
	}
726
727
	/**
728
	 * @param $fieldName
729
	 *
730
	 * @return array|bool
731
	 */
732
	protected function getFieldParams($fieldName)
733
	{
734
		$fields = $this->helper->getFields();
735
736
		if (isset($fields[$fieldName]) && isset($fields[$fieldName]['WIDGET'])) {
737
			return $fields[$fieldName];
738
		}
739
		else {
740
			return false;
741
		}
742
	}
743
744
	/**
745
	 * Получение виджета привязанного к полю.
746
	 *
747
	 * @param $fieldName
748
	 *
749
	 * @return HelperWidget|bool
750
	 */
751
	protected function getFieldWidget($fieldName)
752
	{
753
		$field = $this->getFieldParams($fieldName);
754
755
		return isset($field['WIDGET']) ? $field['WIDGET'] : null;
756
	}
757
}