Completed
Pull Request — master (#1)
by Maksim
08:57 queued 07:33
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
                $model->{$unitBasedColumn.$model->getUnitConversionDataPostfix()} = json_encode($model->calculateMultiUnitConversionData($model->attributes[$unitBasedColumn], $model->getMultiUnitFieldUnit($unitBasedColumn), $options['supported_units']));
67
                $model->{$unitBasedColumn} = $model->processMultiUnitFieldChanges($unitBasedColumn, $model->{$unitBasedColumn});
68
            }
69
            //prevent saving of unit columns
70
            foreach ($model->getUnitConversionUnitColumns() as $unitColumn) {
71
                unset($model->attributes[$unitColumn]);
72
            }
73
        });
74
        static::updating(function ($model) {
75
            /**
76
             * @var Model|MultiUnitSupport $model
77
             */
78
            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...
79
                $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...
80
            }
81
        });
82
    }
83
84
    /**
85
     * @param              $value
86
     * @param AbstractUnit $unit
87
     * @param string[]     $requiredUnits
88
     *
89
     * @return array
90
     */
91
    private function calculateMultiUnitConversionData($value, AbstractUnit $unit, $requiredUnits)
92
    {
93
        $conversionData = [];
94
        foreach ($requiredUnits as $requiredUnitClass) {
95
            /**
96
             * @var AbstractUnit $requiredUnit
97
             */
98
            $requiredUnit = new $requiredUnitClass();
99
            $conversionData[$requiredUnit->getSymbol()] = (new $unit($value))->as($requiredUnit);
100
        }
101
102
        return $conversionData;
103
    }
104
105
    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...
106
    {
107
        return json_decode($this->{$field.$this->getUnitConversionDataPostfix()} ?? null);
108
    }
109
110
    /**
111
     * @return string
112
     */
113
    public function getUnitAttributePostfix()
114
    {
115
        return $this->unitAttributePostfix;
116
    }
117
118
    /**
119
     * @return string
120
     */
121
    protected function getUnitConversionDataPostfix()
122
    {
123
        return $this->unitConversionDataPostfix;
124
    }
125
126
    /**
127
     * @return array
128
     */
129
    protected function getMultiUnitColumns()
130
    {
131
        return $this->multiUnitColumns;
132
    }
133
134
    /**
135
     * @param $field
136
     *
137
     * @throws NotSupportedMultiUnitField
138
     *
139
     * @return AbstractUnit[]
140
     */
141
    public function getMultiUnitFieldSupportedUnits($field)
142
    {
143
        if ($this->isMultiUnitField($field)) {
144
            return $this->getMultiUnitColumns()[$field]['supported_units'];
145
        }
146
147
        throw new NotSupportedMultiUnitField($field);
148
    }
149
150
    /**
151
     * @param $field
152
     *
153
     * @throws NotSupportedMultiUnitField
154
     *
155
     * @return AbstractUnit
156
     */
157
    public function getMultiUnitFieldDefaultUnit($field)
158
    {
159
        if ($this->isMultiUnitField($field)) {
160
            $unitClass = $this->getMultiUnitColumns()[$field]['default_unit'];
161
162
            return new $unitClass();
163
        }
164
165
        throw new NotSupportedMultiUnitField($field);
166
    }
167
168
    /**
169
     * @param                   $field
170
     * @param AbstractUnit|null $unit
171
     *
172
     * @throws NotSupportedMultiUnitField
173
     *
174
     * @return mixed
175
     */
176
    public function getMultiUnitFieldValue($field, AbstractUnit $unit = null)
177
    {
178
        if ($this->isMultiUnitField($field)) {
179
            if (isset($this->{$field})) {
180
                if (is_null($unit)) {
181
                    $unit = $this->getMultiUnitFieldUnit($field);
182
                }
183
                $existingConversionData = $this->getMultiUnitExistingConversionData($field);
184
                if (!is_null($existingConversionData) && !is_null($existingConversionData->{$unit->getSymbol()})) {
185
                    return $existingConversionData->{$unit->getSymbol()};
186
                }
187
188
                return ($this->getMultiUnitFieldDefaultUnit($field)->setValue($this->{$field}))->as(new $unit());
189
            } else {
190
                return;
191
            }
192
        }
193
194
        throw new NotSupportedMultiUnitField($field);
195
    }
196
197
    protected function isMultiUnitField($field)
198
    {
199
        return isset($this->getMultiUnitColumns()[$field]);
200
    }
201
202
    /**
203
     * @param $field
204
     *
205
     * @throws NotSupportedMultiUnitField
206
     *
207
     * @return AbstractUnit
208
     */
209
    protected function getMultiUnitFieldUnit($field)
210
    {
211
        if (isset($this->{$field.$this->getUnitAttributePostfix()})) {
212
            foreach ($this->getMultiUnitFieldSupportedUnits($field) as $unitClass) {
213
                /**
214
                 * @var AbstractUnit $unit
215
                 */
216
                $unit = new $unitClass();
217
                if (strtolower($unit->getSymbol()) == strtolower($this->{$field.$this->getUnitAttributePostfix()})) {
218
                    return $unit;
219
                }
220
            }
221
        }
222
223
        return $this->getMultiUnitFieldDefaultUnit($field);
224
    }
225
226
    protected function forgetMultiUnitFieldUnitInput($field)
227
    {
228
        //prevent column_units to by saved to DB
229
        if (isset($this->attributes[$field.$this->getUnitAttributePostfix()])) {
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...
230
            $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...
231
        }
232
    }
233
234
    protected function setMultiUnitFieldUnit($field, AbstractUnit $unit)
235
    {
236
        $this->{$field.$this->getUnitAttributePostfix()} = $unit->getSymbol();
237
        $this->forgetMultiUnitFieldUnitInput($field);
238
    }
239
240
    /**
241
     * @param $field
242
     *
243
     * @throws NotSupportedMultiUnitField
244
     */
245
    protected function resetMultiUnitFieldUnit($field)
246
    {
247
        $this->setMultiUnitFieldUnit($field, $this->getMultiUnitFieldDefaultUnit($field));
248
    }
249
250
    /**
251
     * Determine if a set mutator exists for an attribute.
252
     *
253
     * @param string $key
254
     *
255
     * @return bool
256
     */
257
    public function hasSetMutator($key)
258
    {
259
        if ($this->isMultiUnitField($key)) {
260
            return true;
261
        }
262
263
        return parent::hasSetMutator($key);
264
    }
265
266
    /**
267
     * Set the value of an attribute using its mutator.
268
     *
269
     * @param string $key
270
     * @param mixed  $value
271
     *
272
     * @throws NotSupportedMultiUnitField
273
     *
274
     * @return mixed
275
     */
276
    protected function setMutatedAttributeValue($key, $value)
277
    {
278
        if ($this->isMultiUnitField($key)) {
279
            $value = $this->processMultiUnitFieldChanges($key, $value);
280
            $this->attributes[$key] = $value;
281
282
            return $value;
283
        }
284
285
        parent::setMutatedAttributeValue($key, $value);
286
    }
287
288
    /**
289
     * Detect changes and set proper base value.
290
     *
291
     * @param $field
292
     * @param $value
293
     *
294
     * @throws NotSupportedMultiUnitField
295
     *
296
     * @return mixed
297
     */
298
    private function processMultiUnitFieldChanges($field, $value)
299
    {
300
        $existingConversionData = $this->getMultiUnitExistingConversionData($field);
301
        if (!is_null($existingConversionData)) {
302
            $inputUnit = $this->getMultiUnitFieldUnit($field);
303
            //change existing value only in case if new value doesn't match with stored conversion table or not exists
304
            if (!isset($existingConversionData->{$inputUnit->getSymbol()}) || $value != $existingConversionData->{$inputUnit->getSymbol()}) {
305
                $this->resetMultiUnitFieldUnit($field);
306
307
                return (new $inputUnit($value))->as($this->getMultiUnitFieldDefaultUnit($field));
308
            } elseif ($value == $existingConversionData->{$inputUnit->getSymbol()}) {
309
                //forget changes if value actually isn't changed
310
                $this->resetMultiUnitFieldUnit($field);
311
                $originalValue = $existingConversionData->{$this->getMultiUnitFieldDefaultUnit($field)->getSymbol()};
312
                $this->attributes[$field] = $originalValue;
313
                $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...
314
315
                return $originalValue;
316
            }
317
            $this->resetMultiUnitFieldUnit($field);
318
        }
319
320
        return $value;
321
    }
322
323
    /**
324
     * Determine if a get mutator exists for an attribute.
325
     *coo.
326
     *
327
     * @param string $key
328
     *
329
     * @return bool
330
     */
331
    public function hasGetMutator($key)
332
    {
333
        if ($this->isMultiUnitField($key) && isset($this->{$key})) {
334
            return true;
335
        }
336
337
        return parent::hasGetMutator($key);
338
    }
339
340
    /**
341
     * Get the value of an attribute using its mutator.
342
     *
343
     * @param string $key
344
     * @param mixed  $value
345
     *
346
     * @throws NotSupportedMultiUnitField
347
     *
348
     * @return mixed
349
     */
350
    public function mutateAttribute($key, $value)
351
    {
352
        if ($this->isMultiUnitField($key)) {
353
            $requestedUnit = $this->getMultiUnitFieldUnit($key);
354
355
            return $this->getMultiUnitFieldValue($key, new $requestedUnit());
356
        }
357
358
        return parent::mutateAttribute($key, $value);
359
    }
360
}
361