Completed
Push — master ( cfe78f...834b7a )
by Maksim
01:28
created

MultiUnitSupport::getMultiUnitFieldValue()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.0111
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
    protected 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 AbstractUnit|null $unit
184
     *
185
     * @throws NotSupportedMultiUnitField
186
     *
187
     * @return mixed
188
     */
189
    public function getMultiUnitFieldValue($field, AbstractUnit $unit = null)
190
    {
191
        if ($this->isMultiUnitField($field)) {
192
            if (isset($this->{$field})) {
193
                if (is_null($unit)) {
194
                    $unit = $this->getMultiUnitFieldUnit($field);
195
                }
196
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
197
                if (!is_null($existingConversionData) && !is_null($existingConversionData->{$unit->getSymbol()})) {
198
                    return $existingConversionData->{$unit->getSymbol()};
199
                }
200
                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...
201
            } else {
202
                return;
203
            }
204
        }
205
206
        throw new NotSupportedMultiUnitField($field);
207
    }
208
209
    protected function isMultiUnitField($field)
210
    {
211
        return isset($this->getMultiUnitColumns()[$field]);
212
    }
213
214
    /**
215
     * @param $field
216
     *
217
     * @throws NotSupportedMultiUnitField
218
     *
219
     * @return AbstractUnit
220
     */
221
    protected function getMultiUnitFieldUnit($field)
222
    {
223
        if (isset($this->{$field.$this->getUnitAttributePostfix()})) {
224
            foreach ($this->getMultiUnitFieldSupportedUnits($field) as $unitClass) {
225
                /**
226
                 * @var AbstractUnit $unit
227
                 */
228
                $unit = new $unitClass();
229
                if (strtolower($unit->getSymbol()) == strtolower($this->{$field.$this->getUnitAttributePostfix()})) {
230
                    return $unit;
231
                }
232
            }
233
        }
234
235
        return $this->getMultiUnitFieldDefaultUnit($field);
236
    }
237
238
    protected function forgetMultiUnitFieldUnitInput($field)
239
    {
240
        //prevent column_units to by saved to DB
241
        if (isset($this->attributes[$field.$this->getUnitAttributePostfix()])) {
242
            $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...
243
        }
244
    }
245
246
    protected function setMultiUnitFieldUnit($field, AbstractUnit $unit)
247
    {
248
        $this->{$field.$this->getUnitAttributePostfix()} = $unit->getSymbol();
249
        $this->forgetMultiUnitFieldUnitInput($field);
250
    }
251
252
    /**
253
     * @param $field
254
     *
255
     * @throws NotSupportedMultiUnitField
256
     */
257
    protected function resetMultiUnitFieldUnit($field)
258
    {
259
        $this->setMultiUnitFieldUnit($field, $this->getMultiUnitFieldDefaultUnit($field));
260
    }
261
262
    /**
263
     * Determine if a set mutator exists for an attribute.
264
     *
265
     * @param string $key
266
     *
267
     * @return bool
268
     */
269
    public function hasSetMutator($key)
270
    {
271
        if ($this->isMultiUnitField($key)) {
272
            return true;
273
        }
274
275
        return parent::hasSetMutator($key);
276
    }
277
278
    /**
279
     * Set the value of an attribute using its mutator.
280
     *
281
     * @param string $key
282
     * @param mixed  $value
283
     *
284
     * @throws NotSupportedMultiUnitField
285
     *
286
     * @return mixed
287
     */
288
    protected function setMutatedAttributeValue($key, $value)
289
    {
290
        if ($this->isMultiUnitField($key)) {
291
            $value = $this->processMultiUnitFieldChanges($key, $value);
292
            $this->attributes[$key] = $value;
293
294
            return $value;
295
        }
296
297
        parent::setMutatedAttributeValue($key, $value);
298
    }
299
300
    /**
301
     * Detect changes and set proper base value.
302
     *
303
     * @param $field
304
     * @param $value
305
     *
306
     * @throws NotSupportedMultiUnitField
307
     *
308
     * @return mixed
309
     */
310
    private function processMultiUnitFieldChanges($field, $value)
311
    {
312
        $existingConversionData = $this->getMultiUnitExistingConversionData($field);
313
        if (!is_null($existingConversionData)) {
314
            $inputUnit = $this->getMultiUnitFieldUnit($field);
315
            //change existing value only in case if new value doesn't match with stored conversion table or not exists
316
            if (!isset($existingConversionData->{$inputUnit->getSymbol()}) || $value != $existingConversionData->{$inputUnit->getSymbol()}) {
317
                $this->resetMultiUnitFieldUnit($field);
318
319
                return (new $inputUnit($value))->as($this->getMultiUnitFieldDefaultUnit($field));
320
            } elseif ($value == $existingConversionData->{$inputUnit->getSymbol()}) {
321
                //forget changes if value actually isn't changed
322
                $this->resetMultiUnitFieldUnit($field);
323
                $originalValue = $existingConversionData->{$this->getMultiUnitFieldDefaultUnit($field)->getSymbol()};
324
                $this->attributes[$field] = $originalValue;
325
                $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...
326
327
                return $originalValue;
328
            }
329
            $this->resetMultiUnitFieldUnit($field);
330
        }
331
332
        return $value;
333
    }
334
335
    /**
336
     * Determine if a get mutator exists for an attribute.
337
     *coo.
338
     *
339
     * @param string $key
340
     *
341
     * @return bool
342
     */
343
    public function hasGetMutator($key)
344
    {
345
        if ($this->isMultiUnitField($key) && isset($this->{$key})) {
346
            return true;
347
        }
348
349
        return parent::hasGetMutator($key);
350
    }
351
352
    /**
353
     * Get the value of an attribute using its mutator.
354
     *
355
     * @param string $key
356
     * @param mixed  $value
357
     *
358
     * @throws NotSupportedMultiUnitField
359
     *
360
     * @return mixed
361
     */
362
    public function mutateAttribute($key, $value)
363
    {
364
        if ($this->isMultiUnitField($key)) {
365
            $requestedUnit = $this->getMultiUnitFieldUnit($key);
366
367
            return $this->getMultiUnitFieldValue($key, new $requestedUnit());
368
        }
369
370
        return parent::mutateAttribute($key, $value);
371
    }
372
}
373