Completed
Push — master ( a2f586...4bbfd3 )
by Maksim
01:10
created

MultiUnitSupport   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 404
Duplicated Lines 1.49 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 1
dl 6
loc 404
rs 4.08
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getUnitConversionDataColumns() 0 6 1
A getUnitConversionUnitColumns() 0 6 1
A fillableFromArray() 0 4 1
A getFillable() 0 4 1
A getHidden() 0 4 1
B bootMultiUnitSupport() 0 38 6
A calculateMultiUnitConversionData() 0 13 2
A getMultiUnitExistingConversionData() 0 4 1
A getUnitAttributePostfix() 0 4 1
A getUnitConversionDataPostfix() 0 4 1
A getMultiUnitColumns() 0 4 1
A getMultiUnitFieldSupportedUnits() 0 8 2
A getMultiUnitFieldDefaultUnit() 0 10 2
B getMultiUnitFieldValueByUnitName() 3 32 9
A getMultiUnitFieldValue() 3 19 6
A isMultiUnitField() 0 4 1
A getMultiUnitFieldUnit() 0 16 4
A forgetMultiUnitFieldUnitInput() 0 7 2
A setMultiUnitFieldUnit() 0 5 1
A resetMultiUnitFieldUnit() 0 4 1
A hasSetMutator() 0 8 2
A setMutatedAttributeValue() 0 11 2
A processMultiUnitFieldChanges() 0 24 5
A hasGetMutator() 0 8 3
A mutateAttribute() 0 10 2

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 MultiUnitSupport 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 MultiUnitSupport, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MaksimM\MultiUnitModels\Traits;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Arr;
7
use MaksimM\MultiUnitModels\Exceptions\NotSupportedMultiUnitField;
8
use UnitConverter\Unit\AbstractUnit;
9
10
trait MultiUnitSupport
11
{
12
    protected $unitAttributePostfix = '_units';
13
    protected $unitConversionDataPostfix = '_ucd';
14
    protected $multiUnitColumns = [];
15
16
    private function getUnitConversionDataColumns()
17
    {
18
        return array_map(function ($column) {
19
            return $column.$this->getUnitConversionDataPostfix();
20
        }, array_keys($this->getMultiUnitColumns()));
21
    }
22
23
    private function getUnitConversionUnitColumns()
24
    {
25
        return array_map(function ($column) {
26
            return $column.$this->getUnitAttributePostfix();
27
        }, array_keys($this->getMultiUnitColumns()));
28
    }
29
30
    /**
31
     * Allows to set input units and process them before multi-unit field.
32
     *
33
     * @param array $attributes
34
     *
35
     * @return array
36
     */
37
    protected function fillableFromArray(array $attributes)
38
    {
39
        return array_merge(array_intersect_key($attributes, array_flip($this->getUnitConversionUnitColumns())), parent::fillableFromArray($attributes));
40
    }
41
42
    /**
43
     * @return array
44
     */
45
    public function getFillable()
46
    {
47
        return array_merge($this->getUnitConversionDataColumns(), $this->getUnitConversionUnitColumns(), parent::getFillable());
48
    }
49
50
    /**
51
     * @return mixed
52
     */
53
    public function getHidden()
54
    {
55
        return array_merge(parent::getHidden(), $this->getUnitConversionDataColumns());
56
    }
57
58
    protected static function bootMultiUnitSupport()
59
    {
60
        //save conversion table if base value is changed
61
        static::creating(function ($model) {
62
            /**
63
             * @var Model|MultiUnitSupport $model
64
             */
65
            foreach ($model->getMultiUnitColumns() as $unitBasedColumn => $options) {
66
                if (isset($model->attributes[$unitBasedColumn])) {
67
                    $model->{$unitBasedColumn.$model->getUnitConversionDataPostfix()} = json_encode(
68
                        $model->calculateMultiUnitConversionData(
69
                            $model->attributes[$unitBasedColumn],
70
                            $model->getMultiUnitFieldUnit($unitBasedColumn),
71
                            $options['supported_units']
72
                        )
73
                    );
74
                    $model->{$unitBasedColumn} = $model->processMultiUnitFieldChanges(
75
                        $unitBasedColumn,
76
                        $model->{$unitBasedColumn}
77
                    );
78
                }
79
            }
80
            //prevent saving of unit columns
81
            foreach ($model->getUnitConversionUnitColumns() as $unitColumn) {
82
                if (isset($model->attributes[$unitColumn])) {
83
                    unset($model->attributes[$unitColumn]);
84
                }
85
            }
86
        });
87
        static::updating(function ($model) {
88
            /**
89
             * @var Model|MultiUnitSupport $model
90
             */
91
            foreach (Arr::only($model->getMultiUnitColumns(), array_keys($model->getDirty())) as $unitBasedColumn => $options) {
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
92
                $model->{$unitBasedColumn.$model->getUnitConversionDataPostfix()} = json_encode($model->calculateMultiUnitConversionData($model->getDirty()[$unitBasedColumn], new $options['default_unit'](), $options['supported_units']));
0 ignored issues
show
Bug introduced by
It seems like getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
93
            }
94
        });
95
    }
96
97
    /**
98
     * @param              $value
99
     * @param AbstractUnit $unit
100
     * @param string[]     $requiredUnits
101
     *
102
     * @return array
103
     */
104
    private function calculateMultiUnitConversionData($value, AbstractUnit $unit, $requiredUnits)
105
    {
106
        $conversionData = [];
107
        foreach ($requiredUnits as $requiredUnitClass) {
108
            /**
109
             * @var AbstractUnit $requiredUnit
110
             */
111
            $requiredUnit = new $requiredUnitClass();
112
            $conversionData[$requiredUnit->getSymbol()] = (new $unit($value))->as($requiredUnit);
113
        }
114
115
        return $conversionData;
116
    }
117
118
    public function getMultiUnitExistingConversionData($field)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
119
    {
120
        return json_decode($this->{$field.$this->getUnitConversionDataPostfix()} ?? null);
121
    }
122
123
    /**
124
     * @return string
125
     */
126
    public function getUnitAttributePostfix()
127
    {
128
        return $this->unitAttributePostfix;
129
    }
130
131
    /**
132
     * @return string
133
     */
134
    protected function getUnitConversionDataPostfix()
135
    {
136
        return $this->unitConversionDataPostfix;
137
    }
138
139
    /**
140
     * @return array
141
     */
142
    public function getMultiUnitColumns()
143
    {
144
        return $this->multiUnitColumns;
145
    }
146
147
    /**
148
     * @param $field
149
     *
150
     * @throws NotSupportedMultiUnitField
151
     *
152
     * @return AbstractUnit[]
153
     */
154
    public function getMultiUnitFieldSupportedUnits($field)
155
    {
156
        if ($this->isMultiUnitField($field)) {
157
            return $this->getMultiUnitColumns()[$field]['supported_units'];
158
        }
159
160
        throw new NotSupportedMultiUnitField($field);
161
    }
162
163
    /**
164
     * @param $field
165
     *
166
     * @throws NotSupportedMultiUnitField
167
     *
168
     * @return AbstractUnit
169
     */
170
    public function getMultiUnitFieldDefaultUnit($field)
171
    {
172
        if ($this->isMultiUnitField($field)) {
173
            $unitClass = $this->getMultiUnitColumns()[$field]['default_unit'];
174
175
            return new $unitClass();
176
        }
177
178
        throw new NotSupportedMultiUnitField($field);
179
    }
180
181
    /**
182
     * @param           $field
183
     * @param string    $unit
0 ignored issues
show
Documentation introduced by
Should the type for parameter $unit not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
184
     *
185
     * @throws NotSupportedMultiUnitField
186
     *
187
     * @return mixed
188
     */
189
    public function getMultiUnitFieldValueByUnitName($field, $unit = null)
190
    {
191
        if ($this->isMultiUnitField($field)) {
192
            if (isset($this->{$field})) {
193
                if (is_null($unit)) {
194
                    $unit = $this->getMultiUnitFieldUnit($field);
195
                } else {
196
                    foreach ($this->getMultiUnitFieldSupportedUnits($field) as $unitClass) {
197
                        /**
198
                         * @var AbstractUnit $unit
199
                         */
200
                        $supportedUnit = new $unitClass();
201
                        if (strtolower($supportedUnit->getSymbol()) == strtolower($unit)) {
202
                            $unit = $supportedUnit;
203
                            break;
204
                        }
205
                    }
206
                }
207
                if(is_string($unit))
208
                    throw new NotSupportedMultiUnitField($field);
209
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
210 View Code Duplication
                if (!is_null($existingConversionData) && !is_null($existingConversionData->{$unit->getSymbol()})) {
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...
211
                    return $existingConversionData->{$unit->getSymbol()};
212
                }
213
                return ($this->getMultiUnitFieldDefaultUnit($field)->setValue($this->{$field} ?? $this->attributes[$field]))->as(new $unit());
0 ignored issues
show
Bug introduced by
The property attributes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
214
            } else {
215
                return;
216
            }
217
        }
218
219
        throw new NotSupportedMultiUnitField($field);
220
    }
221
222
    /**
223
     * @param                   $field
224
     * @param AbstractUnit|null $unit
225
     *
226
     * @throws NotSupportedMultiUnitField
227
     *
228
     * @return mixed
229
     */
230
    public function getMultiUnitFieldValue($field, AbstractUnit $unit = null)
231
    {
232
        if ($this->isMultiUnitField($field)) {
233
            if (isset($this->{$field})) {
234
                if (is_null($unit)) {
235
                    $unit = $this->getMultiUnitFieldUnit($field);
236
                }
237
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
238 View Code Duplication
                if (!is_null($existingConversionData) && !is_null($existingConversionData->{$unit->getSymbol()})) {
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...
239
                    return $existingConversionData->{$unit->getSymbol()};
240
                }
241
                return ($this->getMultiUnitFieldDefaultUnit($field)->setValue($this->{$field} ?? $this->attributes[$field]))->as(new $unit());
242
            } else {
243
                return;
244
            }
245
        }
246
247
        throw new NotSupportedMultiUnitField($field);
248
    }
249
250
    protected function isMultiUnitField($field)
251
    {
252
        return isset($this->getMultiUnitColumns()[$field]);
253
    }
254
255
    /**
256
     * @param $field
257
     *
258
     * @throws NotSupportedMultiUnitField
259
     *
260
     * @return AbstractUnit
261
     */
262
    protected function getMultiUnitFieldUnit($field)
263
    {
264
        if (isset($this->{$field.$this->getUnitAttributePostfix()})) {
265
            foreach ($this->getMultiUnitFieldSupportedUnits($field) as $unitClass) {
266
                /**
267
                 * @var AbstractUnit $unit
268
                 */
269
                $unit = new $unitClass();
270
                if (strtolower($unit->getSymbol()) == strtolower($this->{$field.$this->getUnitAttributePostfix()})) {
271
                    return $unit;
272
                }
273
            }
274
        }
275
276
        return $this->getMultiUnitFieldDefaultUnit($field);
277
    }
278
279
    protected function forgetMultiUnitFieldUnitInput($field)
280
    {
281
        //prevent column_units to by saved to DB
282
        if (isset($this->attributes[$field.$this->getUnitAttributePostfix()])) {
283
            $this->syncOriginalAttribute($field.$this->getUnitAttributePostfix());
0 ignored issues
show
Bug introduced by
It seems like syncOriginalAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
284
        }
285
    }
286
287
    protected function setMultiUnitFieldUnit($field, AbstractUnit $unit)
288
    {
289
        $this->{$field.$this->getUnitAttributePostfix()} = $unit->getSymbol();
290
        $this->forgetMultiUnitFieldUnitInput($field);
291
    }
292
293
    /**
294
     * @param $field
295
     *
296
     * @throws NotSupportedMultiUnitField
297
     */
298
    protected function resetMultiUnitFieldUnit($field)
299
    {
300
        $this->setMultiUnitFieldUnit($field, $this->getMultiUnitFieldDefaultUnit($field));
301
    }
302
303
    /**
304
     * Determine if a set mutator exists for an attribute.
305
     *
306
     * @param string $key
307
     *
308
     * @return bool
309
     */
310
    public function hasSetMutator($key)
311
    {
312
        if ($this->isMultiUnitField($key)) {
313
            return true;
314
        }
315
316
        return parent::hasSetMutator($key);
317
    }
318
319
    /**
320
     * Set the value of an attribute using its mutator.
321
     *
322
     * @param string $key
323
     * @param mixed  $value
324
     *
325
     * @throws NotSupportedMultiUnitField
326
     *
327
     * @return mixed
328
     */
329
    protected function setMutatedAttributeValue($key, $value)
330
    {
331
        if ($this->isMultiUnitField($key)) {
332
            $value = $this->processMultiUnitFieldChanges($key, $value);
333
            $this->attributes[$key] = $value;
334
335
            return $value;
336
        }
337
338
        parent::setMutatedAttributeValue($key, $value);
339
    }
340
341
    /**
342
     * Detect changes and set proper base value.
343
     *
344
     * @param $field
345
     * @param $value
346
     *
347
     * @throws NotSupportedMultiUnitField
348
     *
349
     * @return mixed
350
     */
351
    private function processMultiUnitFieldChanges($field, $value)
352
    {
353
        $existingConversionData = $this->getMultiUnitExistingConversionData($field);
354
        if (!is_null($existingConversionData)) {
355
            $inputUnit = $this->getMultiUnitFieldUnit($field);
356
            //change existing value only in case if new value doesn't match with stored conversion table or not exists
357
            if (!isset($existingConversionData->{$inputUnit->getSymbol()}) || $value != $existingConversionData->{$inputUnit->getSymbol()}) {
358
                $this->resetMultiUnitFieldUnit($field);
359
360
                return (new $inputUnit($value))->as($this->getMultiUnitFieldDefaultUnit($field));
361
            } elseif ($value == $existingConversionData->{$inputUnit->getSymbol()}) {
362
                //forget changes if value actually isn't changed
363
                $this->resetMultiUnitFieldUnit($field);
364
                $originalValue = $existingConversionData->{$this->getMultiUnitFieldDefaultUnit($field)->getSymbol()};
365
                $this->attributes[$field] = $originalValue;
366
                $this->syncOriginalAttribute($field);
0 ignored issues
show
Bug introduced by
It seems like syncOriginalAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
367
368
                return $originalValue;
369
            }
370
            $this->resetMultiUnitFieldUnit($field);
371
        }
372
373
        return $value;
374
    }
375
376
    /**
377
     * Determine if a get mutator exists for an attribute.
378
     *coo.
379
     *
380
     * @param string $key
381
     *
382
     * @return bool
383
     */
384
    public function hasGetMutator($key)
385
    {
386
        if ($this->isMultiUnitField($key) && isset($this->{$key})) {
387
            return true;
388
        }
389
390
        return parent::hasGetMutator($key);
391
    }
392
393
    /**
394
     * Get the value of an attribute using its mutator.
395
     *
396
     * @param string $key
397
     * @param mixed  $value
398
     *
399
     * @throws NotSupportedMultiUnitField
400
     *
401
     * @return mixed
402
     */
403
    public function mutateAttribute($key, $value)
404
    {
405
        if ($this->isMultiUnitField($key)) {
406
            $requestedUnit = $this->getMultiUnitFieldUnit($key);
407
408
            return $this->getMultiUnitFieldValue($key, new $requestedUnit());
409
        }
410
411
        return parent::mutateAttribute($key, $value);
412
    }
413
}
414