Completed
Pull Request — master (#3)
by Maksim
02:39 queued 01:19
created

MultiUnitSupport::getMultiUnitFieldValue()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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