Completed
Pull Request — master (#5)
by Maksim
02:00 queued 01:02
created

MultiUnitSupport::getMultiUnitFieldValue()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 20

Duplication

Lines 3
Ratio 15 %

Importance

Changes 0
Metric Value
dl 3
loc 20
rs 8.9777
c 0
b 0
f 0
cc 6
nc 6
nop 2
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
                }
210
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
211 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...
212
                    return $existingConversionData->{$unit->getSymbol()};
213
                }
214
215
                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...
216
            } else {
217
                return;
218
            }
219
        }
220
221
        throw new NotSupportedMultiUnitField($field);
222
    }
223
224
    /**
225
     * @param                   $field
226
     * @param AbstractUnit|null $unit
227
     *
228
     * @throws NotSupportedMultiUnitField
229
     *
230
     * @return mixed
231
     */
232
    public function getMultiUnitFieldValue($field, AbstractUnit $unit = null)
233
    {
234
        if ($this->isMultiUnitField($field)) {
235
            if (isset($this->{$field})) {
236
                if (is_null($unit)) {
237
                    $unit = $this->getMultiUnitFieldUnit($field);
238
                }
239
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
240 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...
241
                    return $existingConversionData->{$unit->getSymbol()};
242
                }
243
244
                return ($this->getMultiUnitFieldDefaultUnit($field)->setValue($this->{$field} ?? $this->attributes[$field]))->as(new $unit());
245
            } else {
246
                return;
247
            }
248
        }
249
250
        throw new NotSupportedMultiUnitField($field);
251
    }
252
253
    protected function isMultiUnitField($field)
254
    {
255
        return isset($this->getMultiUnitColumns()[$field]);
256
    }
257
258
    /**
259
     * @param $field
260
     *
261
     * @throws NotSupportedMultiUnitField
262
     *
263
     * @return AbstractUnit
264
     */
265
    protected function getMultiUnitFieldUnit($field)
266
    {
267
        if (isset($this->{$field.$this->getUnitAttributePostfix()})) {
268
            foreach ($this->getMultiUnitFieldSupportedUnits($field) as $unitClass) {
269
                /**
270
                 * @var AbstractUnit $unit
271
                 */
272
                $unit = new $unitClass();
273
                if (strtolower($unit->getSymbol()) == strtolower($this->{$field.$this->getUnitAttributePostfix()})) {
274
                    return $unit;
275
                }
276
            }
277
        }
278
279
        return $this->getMultiUnitFieldDefaultUnit($field);
280
    }
281
282
    protected function forgetMultiUnitFieldUnitInput($field)
283
    {
284
        //prevent column_units to by saved to DB
285
        if (isset($this->attributes[$field.$this->getUnitAttributePostfix()])) {
286
            $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...
287
        }
288
    }
289
290
    protected function setMultiUnitFieldUnit($field, AbstractUnit $unit)
291
    {
292
        $this->{$field.$this->getUnitAttributePostfix()} = $unit->getSymbol();
293
        $this->forgetMultiUnitFieldUnitInput($field);
294
    }
295
296
    /**
297
     * @param $field
298
     *
299
     * @throws NotSupportedMultiUnitField
300
     */
301
    protected function resetMultiUnitFieldUnit($field)
302
    {
303
        $this->setMultiUnitFieldUnit($field, $this->getMultiUnitFieldDefaultUnit($field));
304
    }
305
306
    /**
307
     * Determine if a set mutator exists for an attribute.
308
     *
309
     * @param string $key
310
     *
311
     * @return bool
312
     */
313
    public function hasSetMutator($key)
314
    {
315
        if ($this->isMultiUnitField($key)) {
316
            return true;
317
        }
318
319
        return parent::hasSetMutator($key);
320
    }
321
322
    /**
323
     * Set the value of an attribute using its mutator.
324
     *
325
     * @param string $key
326
     * @param mixed  $value
327
     *
328
     * @throws NotSupportedMultiUnitField
329
     *
330
     * @return mixed
331
     */
332
    protected function setMutatedAttributeValue($key, $value)
333
    {
334
        if ($this->isMultiUnitField($key)) {
335
            $value = $this->processMultiUnitFieldChanges($key, $value);
336
            $this->attributes[$key] = $value;
337
338
            return $value;
339
        }
340
341
        parent::setMutatedAttributeValue($key, $value);
342
    }
343
344
    /**
345
     * Detect changes and set proper base value.
346
     *
347
     * @param $field
348
     * @param $value
349
     *
350
     * @throws NotSupportedMultiUnitField
351
     *
352
     * @return mixed
353
     */
354
    private function processMultiUnitFieldChanges($field, $value)
355
    {
356
        $existingConversionData = $this->getMultiUnitExistingConversionData($field);
357
        if (!is_null($existingConversionData)) {
358
            $inputUnit = $this->getMultiUnitFieldUnit($field);
359
            //change existing value only in case if new value doesn't match with stored conversion table or not exists
360
            if (!isset($existingConversionData->{$inputUnit->getSymbol()}) || $value != $existingConversionData->{$inputUnit->getSymbol()}) {
361
                $this->resetMultiUnitFieldUnit($field);
362
363
                return (new $inputUnit($value))->as($this->getMultiUnitFieldDefaultUnit($field));
364
            } elseif ($value == $existingConversionData->{$inputUnit->getSymbol()}) {
365
                //forget changes if value actually isn't changed
366
                $this->resetMultiUnitFieldUnit($field);
367
                $originalValue = $existingConversionData->{$this->getMultiUnitFieldDefaultUnit($field)->getSymbol()};
368
                $this->attributes[$field] = $originalValue;
369
                $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...
370
371
                return $originalValue;
372
            }
373
            $this->resetMultiUnitFieldUnit($field);
374
        }
375
376
        return $value;
377
    }
378
379
    /**
380
     * Determine if a get mutator exists for an attribute.
381
     *coo.
382
     *
383
     * @param string $key
384
     *
385
     * @return bool
386
     */
387
    public function hasGetMutator($key)
388
    {
389
        if ($this->isMultiUnitField($key) && isset($this->{$key})) {
390
            return true;
391
        }
392
393
        return parent::hasGetMutator($key);
394
    }
395
396
    /**
397
     * Get the value of an attribute using its mutator.
398
     *
399
     * @param string $key
400
     * @param mixed  $value
401
     *
402
     * @throws NotSupportedMultiUnitField
403
     *
404
     * @return mixed
405
     */
406
    public function mutateAttribute($key, $value)
407
    {
408
        if ($this->isMultiUnitField($key)) {
409
            $requestedUnit = $this->getMultiUnitFieldUnit($key);
410
411
            return $this->getMultiUnitFieldValue($key, new $requestedUnit());
412
        }
413
414
        return parent::mutateAttribute($key, $value);
415
    }
416
}
417