Completed
Push — master ( b41bcf...0c59ec )
by Nekrasov
01:58
created

ElementModel   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 503
Duplicated Lines 5.77 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 64
lcom 2
cbo 4
dl 29
loc 503
rs 3.28
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A iblockId() 0 9 2
A internalDirectCreate() 0 4 1
A sectionModel() 0 4 1
A query() 0 4 1
A scopeSortByDate() 0 4 1
A scopeFromSectionWithId() 0 6 1
A scopeFromSectionWithCode() 0 6 1
A afterFill() 0 4 1
A load() 0 6 1
A getSections() 0 8 2
A refresh() 0 4 1
A refreshFields() 21 21 4
A refreshSections() 0 16 3
A getSection() 0 15 3
A section() 0 11 2
A getPanelButtons() 0 9 1
A saveProps() 0 16 3
A normalizePropertyFormat() 0 14 3
A create() 8 8 2
C constructPropertyValuesForSave() 0 47 15
A fieldShouldNotBeSaved() 0 14 4
A internalUpdate() 0 15 6
A getValueFromLanguageField() 0 6 2
A setWorkflow() 0 4 1
A setUpdateSearch() 0 4 1
A setResizePictures() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ElementModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ElementModel, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Arrilot\BitrixModels\Models;
4
5
use Arrilot\BitrixModels\Exceptions\ExceptionFromBitrix;
6
use Arrilot\BitrixModels\Queries\ElementQuery;
7
use CIBlock;
8
use Illuminate\Support\Collection;
9
use LogicException;
10
11
/**
12
 * ElementQuery methods
13
 * @method static ElementQuery groupBy($value)
14
 * @method static static getByCode(string $code)
15
 * @method static static getByExternalId(string $id)
16
 *
17
 * Base Query methods
18
 * @method static Collection|static[] getList()
19
 * @method static static first()
20
 * @method static static getById(int $id)
21
 * @method static ElementQuery sort(string|array $by, string $order='ASC')
22
 * @method static ElementQuery order(string|array $by, string $order='ASC') // same as sort()
23
 * @method static ElementQuery filter(array $filter)
24
 * @method static ElementQuery addFilter(array $filters)
25
 * @method static ElementQuery resetFilter()
26
 * @method static ElementQuery navigation(array $filter)
27
 * @method static ElementQuery select($value)
28
 * @method static ElementQuery keyBy(string $value)
29
 * @method static ElementQuery limit(int $value)
30
 * @method static ElementQuery offset(int $value)
31
 * @method static ElementQuery page(int $num)
32
 * @method static ElementQuery take(int $value) // same as limit()
33
 * @method static ElementQuery forPage(int $page, int $perPage=15)
34
 * @method static \Illuminate\Pagination\LengthAwarePaginator paginate(int $perPage = 15, string $pageName = 'page')
35
 * @method static \Illuminate\Pagination\Paginator simplePaginate(int $perPage = 15, string $pageName = 'page')
36
 * @method static ElementQuery stopQuery()
37
 * @method static ElementQuery cache(float|int $minutes)
38
 *
39
 * Scopes
40
 * @method static ElementQuery active()
41
 * @method static ElementQuery sortByDate(string $sort = 'DESC')
42
 * @method static ElementQuery fromSectionWithId(int $id)
43
 * @method static ElementQuery fromSectionWithCode(string $code)
44
 */
45
class ElementModel extends BitrixModel
46
{
47
    /**
48
     * Corresponding IBLOCK_ID
49
     *
50
     * @var int
51
     */
52
    const IBLOCK_ID = null;
53
54
    /**
55
     * IBLOCK version (1 or 2)
56
     *
57
     * @var int
58
     */
59
    const IBLOCK_VERSION = 2;
60
61
    /**
62
     * Bitrix entity object.
63
     *
64
     * @var object
65
     */
66
    public static $bxObject;
67
68
    /**
69
     * Corresponding object class name.
70
     *
71
     * @var string
72
     */
73
    protected static $objectClass = 'CIBlockElement';
74
75
    /**
76
     * Have sections been already fetched from DB?
77
     *
78
     * @var bool
79
     */
80
    protected $sectionsAreFetched = false;
81
82
    /**
83
     * Log in Bitrix workflow ($bWorkFlow for CIBlockElement::Add/Update).
84
     *
85
     * @var bool
86
     */
87
    protected static $workFlow = false;
88
89
    /**
90
     * Update search after each create or update ($bUpdateSearch for CIBlockElement::Add/Update).
91
     *
92
     * @var bool
93
     */
94
    protected static $updateSearch = true;
95
96
    /**
97
     * Resize pictures during add/update ($bResizePictures for CIBlockElement::Add/Update).
98
     *
99
     * @var bool
100
     */
101
    protected static $resizePictures = false;
102
103
    /**
104
     * Getter for corresponding iblock id.
105
     *
106
     * @throws LogicException
107
     *
108
     * @return int
109
     */
110
    public static function iblockId()
111
    {
112
        $id = static::IBLOCK_ID;
113
        if (!$id) {
114
            throw new LogicException('You must set IBLOCK_ID constant inside a model or override iblockId() method');
115
        }
116
        
117
        return $id;
118
    }
119
    
120
    /**
121
     * Create new item in database.
122
     *
123
     * @param $fields
124
     *
125
     * @throws LogicException
126
     *
127
     * @return static|bool
128
     * @throws ExceptionFromBitrix
129
     */
130 View Code Duplication
    public static function create($fields)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
131
    {
132
        if (!isset($fields['IBLOCK_ID'])) {
133
            $fields['IBLOCK_ID'] = static::iblockId();
134
        }
135
136
        return static::internalCreate($fields);
137
    }
138
139
    public static function internalDirectCreate($bxObject, $fields)
140
    {
141
        return $bxObject->add($fields, static::$workFlow, static::$updateSearch, static::$resizePictures);
142
    }
143
144
    /**
145
     * Corresponding section model full qualified class name.
146
     * MUST be overridden if you are going to use section model for this iblock.
147
     *
148
     * @throws LogicException
149
     *
150
     * @return string
151
     */
152
    public static function sectionModel()
153
    {
154
        throw new LogicException('public static function sectionModel() MUST be overridden');
155
    }
156
157
    /**
158
     * Instantiate a query object for the model.
159
     *
160
     * @return ElementQuery
161
     */
162
    public static function query()
163
    {
164
        return new ElementQuery(static::instantiateObject(), get_called_class());
165
    }
166
167
    /**
168
     * Scope to sort by date.
169
     *
170
     * @param ElementQuery $query
171
     * @param string       $sort
172
     *
173
     * @return ElementQuery
174
     */
175
    public function scopeSortByDate($query, $sort = 'DESC')
176
    {
177
        return $query->sort(['ACTIVE_FROM' => $sort]);
178
    }
179
180
    /**
181
     * Scope to get only items from a given section.
182
     *
183
     * @param ElementQuery $query
184
     * @param mixed        $id
185
     *
186
     * @return ElementQuery
187
     */
188
    public function scopeFromSectionWithId($query, $id)
189
    {
190
        $query->filter['SECTION_ID'] = $id;
191
192
        return $query;
193
    }
194
195
    /**
196
     * Scope to get only items from a given section.
197
     *
198
     * @param ElementQuery $query
199
     * @param string       $code
200
     *
201
     * @return ElementQuery
202
     */
203
    public function scopeFromSectionWithCode($query, $code)
204
    {
205
        $query->filter['SECTION_CODE'] = $code;
206
207
        return $query;
208
    }
209
210
    /**
211
     * Fill extra fields when $this->field is called.
212
     *
213
     * @return null
214
     */
215
    protected function afterFill()
216
    {
217
        $this->normalizePropertyFormat();
218
    }
219
220
    /**
221
     * Load all model attributes from cache or database.
222
     *
223
     * @return $this
224
     */
225
    public function load()
226
    {
227
        $this->getFields();
228
229
        return $this;
230
    }
231
232
    /**
233
     * Get element's sections from cache or database.
234
     *
235
     * @return array
236
     */
237
    public function getSections()
238
    {
239
        if ($this->sectionsAreFetched) {
240
            return $this->fields['IBLOCK_SECTION'];
241
        }
242
243
        return $this->refreshSections();
244
    }
245
246
    /**
247
     * Refresh model from database and place data to $this->fields.
248
     *
249
     * @return array
250
     */
251
    public function refresh()
252
    {
253
        return $this->refreshFields();
254
    }
255
256
    /**
257
     * Refresh element's fields and save them to a class field.
258
     *
259
     * @return array
260
     */
261 View Code Duplication
    public function refreshFields()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
262
    {
263
        if ($this->id === null) {
264
            $this->original = [];
265
            return $this->fields = [];
266
        }
267
268
        $sectionsBackup = isset($this->fields['IBLOCK_SECTION']) ? $this->fields['IBLOCK_SECTION'] : null;
269
270
        $this->fields = static::query()->getById($this->id)->fields;
271
272
        if (!empty($sectionsBackup)) {
273
            $this->fields['IBLOCK_SECTION'] = $sectionsBackup;
274
        }
275
276
        $this->fieldsAreFetched = true;
277
278
        $this->original = $this->fields;
279
280
        return $this->fields;
281
    }
282
283
    /**
284
     * Refresh element's sections and save them to a class field.
285
     *
286
     * @return array
287
     */
288
    public function refreshSections()
289
    {
290
        if ($this->id === null) {
291
            return [];
292
        }
293
294
        $this->fields['IBLOCK_SECTION'] = [];
295
        $dbSections = static::$bxObject->getElementGroups($this->id, true);
296
        while ($section = $dbSections->Fetch()) {
297
            $this->fields['IBLOCK_SECTION'][] = $section;
298
        }
299
300
        $this->sectionsAreFetched = true;
301
302
        return $this->fields['IBLOCK_SECTION'];
303
    }
304
305
    /**
306
     * @deprecated in favour of `->section()`
307
     * Get element direct section as ID or array of fields.
308
     *
309
     * @param bool $load
310
     *
311
     * @return false|int|array
312
     */
313
    public function getSection($load = false)
314
    {
315
        $fields = $this->getFields();
316
        if (!$load) {
317
            return $fields['IBLOCK_SECTION_ID'];
318
        }
319
320
        /** @var SectionModel $sectionModel */
321
        $sectionModel = static::sectionModel();
322
        if (!$fields['IBLOCK_SECTION_ID']) {
323
            return false;
324
        }
325
326
        return $sectionModel::query()->getById($fields['IBLOCK_SECTION_ID'])->toArray();
327
    }
328
329
    /**
330
     * Get element direct section as model object.
331
     *
332
     * @param bool $load
333
     *
334
     * @return false|SectionModel
335
     */
336
    public function section($load = false)
337
    {
338
        $fields = $this->getFields();
339
340
        /** @var SectionModel $sectionModel */
341
        $sectionModel = static::sectionModel();
342
343
        return $load
344
            ? $sectionModel::query()->getById($fields['IBLOCK_SECTION_ID'])
345
            : new $sectionModel($fields['IBLOCK_SECTION_ID']);
346
    }
347
348
    /**
349
     * Proxy for GetPanelButtons
350
     *
351
     * @param array $options
352
     * @return array
353
     */
354
    public function getPanelButtons($options = [])
355
    {
356
        return CIBlock::GetPanelButtons(
357
            static::iblockId(),
358
            $this->id,
359
            0,
360
            $options
361
        );
362
    }
363
364
    /**
365
     * Save props to database.
366
     * If selected is not empty then only props from it are saved.
367
     *
368
     * @param array $selected
369
     *
370
     * @return bool
371
     */
372
    public function saveProps($selected = [])
373
    {
374
        $propertyValues = $this->constructPropertyValuesForSave($selected);
375
        if (empty($propertyValues)) {
376
            return false;
377
        }
378
379
        $bxMethod = empty($selected) ? 'setPropertyValues' : 'setPropertyValuesEx';
380
        static::$bxObject->$bxMethod(
381
            $this->id,
382
            static::iblockId(),
383
            $propertyValues
384
        );
385
386
        return true;
387
    }
388
389
    /**
390
     * Normalize properties's format converting it to 'PROPERTY_"CODE"_VALUE'.
391
     *
392
     * @return null
393
     */
394
    protected function normalizePropertyFormat()
395
    {
396
        if (empty($this->fields['PROPERTIES'])) {
397
            return;
398
        }
399
400
        foreach ($this->fields['PROPERTIES'] as $code => $prop) {
401
            $this->fields['PROPERTY_'.$code.'_VALUE'] = $prop['VALUE'];
402
            $this->fields['~PROPERTY_'.$code.'_VALUE'] = $prop['~VALUE'];
403
            $this->fields['PROPERTY_'.$code.'_DESCRIPTION'] = $prop['DESCRIPTION'];
404
            $this->fields['~PROPERTY_'.$code.'_DESCRIPTION'] = $prop['~DESCRIPTION'];
405
            $this->fields['PROPERTY_'.$code.'_VALUE_ID'] = $prop['PROPERTY_VALUE_ID'];
406
        }
407
    }
408
409
    /**
410
     * Construct 'PROPERTY_VALUES' => [...] from flat fields array.
411
     * This is used in save.
412
     * If $selectedFields are specified only those are saved.
413
     *
414
     * @param $selectedFields
415
     *
416
     * @return array
417
     */
418
    protected function constructPropertyValuesForSave($selectedFields = [])
419
    {
420
        $propertyValues = [];
421
        $saveOnlySelected = !empty($selectedFields);
422
423
        // if we pass PROPERTY_X_DESCRIPTION as selected field, we need to add PROPERTY_X_VALUE as well.
424
        if ($saveOnlySelected) {
425
            foreach ($selectedFields as $code) {
426
                if (preg_match('/^PROPERTY_(.*)_DESCRIPTION$/', $code, $matches) && !empty($matches[1])) {
427
                    $propertyCode = $matches[1];
428
                    $propertyValueKey = "PROPERTY_{$propertyCode}_VALUE";
429
                    if (!in_array($propertyValueKey, $selectedFields)) {
430
                        $selectedFields[] = $propertyValueKey;
431
                    }
432
                }
433
            }
434
        }
435
436
        foreach ($this->fields as $code => $value) {
0 ignored issues
show
Bug introduced by
The expression $this->fields 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...
437
            if ($saveOnlySelected && !in_array($code, $selectedFields)) {
438
                continue;
439
            }
440
441
            if (preg_match('/^PROPERTY_(.*)_VALUE$/', $code, $matches) && !empty($matches[1])) {
442
                $propertyCode = $matches[1];
443
444
                // if property values have descriptions
445
                if (array_key_exists("PROPERTY_{$propertyCode}_DESCRIPTION", $this->fields)) {
446
                    $description = $this->fields["PROPERTY_{$propertyCode}_DESCRIPTION"];
447
448
                    if (is_array($value) && is_array($description)) {
449
                        // for multiple property
450
                        foreach ($value as $rowIndex => $rowValue) {
451
                            $propertyValues[$propertyCode][] = ['VALUE' => $rowValue, 'DESCRIPTION' => $description[$rowIndex]];
452
                        }
453
                    } else {
454
                        // for single property
455
                        $propertyValues[$propertyCode] = ['VALUE' => $value, 'DESCRIPTION' => $description];
456
                    }
457
                } else {
458
                    $propertyValues[$propertyCode] = $value;
459
                }
460
            }
461
        }
462
463
        return $propertyValues;
464
    }
465
466
    /**
467
     * Determine whether the field should be stopped from passing to "update".
468
     *
469
     * @param string $field
470
     * @param mixed  $value
471
     * @param array  $selectedFields
472
     *
473
     * @return bool
474
     */
475
    protected function fieldShouldNotBeSaved($field, $value, $selectedFields)
476
    {
477
        $blacklistedFields = [
478
            'ID',
479
            'IBLOCK_ID',
480
            'PROPERTIES',
481
            'PROPERTY_VALUES',
482
        ];
483
484
        return (!empty($selectedFields) && !in_array($field, $selectedFields))
485
            || in_array($field, $blacklistedFields)
486
            || ($field[0] === '~');
487
            //|| (substr($field, 0, 9) === 'PROPERTY_');
488
    }
489
490
    /**
491
     * @param $fields
492
     * @param $fieldsSelectedForSave
493
     * @return bool
494
     */
495
    protected function internalUpdate($fields, $fieldsSelectedForSave)
496
    {
497
        $fields = $fields ?: [];
498
        foreach ($fields as $key => $value) {
499
            if (substr($key, 0, 9) === 'PROPERTY_') {
500
                unset($fields[$key]);
501
            }
502
        }
503
504
        $result = !empty($fields) ? static::$bxObject->update($this->id, $fields, static::$workFlow, static::$updateSearch, static::$resizePictures) : false;
505
        $savePropsResult = $this->saveProps($fieldsSelectedForSave);
506
        $result = $result || $savePropsResult;
507
508
        return $result;
509
    }
510
511
    /**
512
     * Get value from language field according to current language.
513
     *
514
     * @param $field
515
     * @return mixed
516
     */
517
    protected function getValueFromLanguageField($field)
518
    {
519
        $key = $field . '_' . $this->getCurrentLanguage() . '_VALUE';
520
521
        return isset($this->fields[$key]) ? $this->fields[$key] : null;
522
    }
523
524
    /**
525
     * @param $value
526
     */
527
    public static function setWorkflow($value)
528
    {
529
        static::$workFlow = $value;
530
    }
531
532
    /**
533
     * @param $value
534
     */
535
    public static function setUpdateSearch($value)
536
    {
537
        static::$updateSearch = $value;
538
    }
539
540
    /**
541
     * @param $value
542
     */
543
    public static function setResizePictures($value)
544
    {
545
        static::$resizePictures = $value;
546
    }
547
}
548