Attribute::getMutator()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 10

Duplication

Lines 5
Ratio 50 %

Importance

Changes 0
Metric Value
dl 5
loc 10
rs 9.6111
c 0
b 0
f 0
cc 5
nc 6
nop 3
1
<?php
2
3
namespace Sofa\Eloquence\Metable;
4
5
use InvalidArgumentException;
6
use Illuminate\Database\Eloquent\Model;
7
use Sofa\Eloquence\Contracts\Attribute as AttributeContract;
8
use Sofa\Eloquence\Mutator\Mutator;
9
10
/**
11
 * @property array $attributes
12
 */
13
class Attribute extends Model implements AttributeContract
14
{
15
    /**
16
     * Attribute mutator instance.
17
     *
18
     * @var \Sofa\Eloquence\Contracts\Mutator
19
     */
20
    protected static $attributeMutator;
21
22
    /**
23
     * Custom table name.
24
     *
25
     * @var string
26
     */
27
    protected static $customTable;
28
29
    /**
30
     * The database table used by the model.
31
     *
32
     * @var string
33
     */
34
    protected $table = 'meta_attributes';
35
36
    /**
37
     * Indicates if the model should be timestamped.
38
     *
39
     * @var bool
40
     */
41
    public $timestamps = false;
42
43
    /**
44
     * The primary key for the model.
45
     *
46
     * @var string
47
     */
48
    protected $primaryKey = 'meta_id';
49
50
    /**
51
     * @var array
52
     */
53
    protected $getterMutators = [
54
        'array' => 'json_decode',
55
        'StdClass' => 'json_decode',
56
        'DateTime' => 'asDateTime',
57
        Model::class => 'unserialize',
58
    ];
59
60
    /**
61
     * @var array
62
     */
63
    protected $setterMutators = [
64
        'array' => 'json_encode',
65
        'StdClass' => 'json_encode',
66
        'DateTime' => 'fromDateTime',
67
        Model::class => 'serialize',
68
    ];
69
70
    /**
71
     * The attributes included in the model's JSON and array form.
72
     *
73
     * @var array
74
     */
75
    protected $visible = ['meta_key', 'meta_value', 'meta_type', 'meta_group'];
76
77
    /**
78
     * Create new attribute instance.
79
     *
80
     * @param string|array  $key
81
     * @param mixed  $value
82
     */
83
    public function __construct($key = null, $value = null, $group = null)
84
    {
85
        // default behaviour
86
        if (is_array($key)) {
87
            parent::__construct($key);
88
        } else {
89
            parent::__construct();
90
91
            if (is_string($key)) {
92
                $this->set($key, $value, $group);
93
            }
94
        }
95
    }
96
97
    /**
98
     * Boot this model.
99
     *
100
     * @codeCoverageIgnore
101
     *
102
     * @return void
103
     */
104
    protected static function boot()
105
    {
106
        parent::boot();
107
108
        if (!isset(static::$attributeMutator)) {
109
            if (function_exists('app') && app()->bound('eloquence.mutator')) {
110
                static::$attributeMutator = app('eloquence.mutator');
111
            } else {
112
                static::$attributeMutator = new Mutator;
113
            }
114
        }
115
    }
116
117
    /**
118
     * Set the meta attribute.
119
     *
120
     * @param string $key
121
     * @param mixed  $value
122
     * @param string $group
123
     */
124
    protected function set($key, $value, $group = 'default')
125
    {
126
        $this->setMetaKey($key);
127
        $this->setValue($value);
128
        $this->setMetaGroup($group);
129
    }
130
131
    /**
132
     * Create new AttributeBag.
133
     *
134
     * @param  array  $models
135
     * @return \Sofa\Eloquence\Metable\AttributeBag
136
     */
137
    public function newBag(array $models = [])
138
    {
139
        return new AttributeBag($models);
140
    }
141
142
    /**
143
     * Get the meta attribute value.
144
     *
145
     * @return mixed
146
     */
147
    public function getValue()
148
    {
149
        if ($this->hasMutator($this->attributes['meta_value'], 'getter', $this->attributes['meta_type'])) {
150
            return $this->mutateValue($this->attributes['meta_value'], 'getter');
151
        }
152
153
        return $this->castValue();
154
    }
155
156
    /**
157
     * Get the meta attribute key.
158
     *
159
     * @return string
160
     */
161
    public function getMetaKey()
162
    {
163
        return $this->attributes['meta_key'];
164
    }
165
166
    /**
167
     * Get the meta attribute group.
168
     *
169
     * @return string
170
     */
171
    public function getMetaGroup()
172
    {
173
        return $this->attributes['meta_group'];
174
    }
175
176
    /**
177
     * Cast value to proper type.
178
     *
179
     * @return mixed
180
     */
181
    protected function castValue()
182
    {
183
        $value = $this->attributes['meta_value'];
184
185
        $validTypes = ['boolean', 'integer', 'float', 'double', 'array', 'object', 'null'];
186
187
        if (in_array($this->attributes['meta_type'], $validTypes)) {
188
            settype($value, $this->attributes['meta_type']);
189
        }
190
191
        return $value;
192
    }
193
194
    /**
195
     * Set key of the meta attribute.
196
     *
197
     * @param string $key
198
     *
199
     * @throws \InvalidArgumentException
200
     */
201
    protected function setMetaKey($key)
202
    {
203
        if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $key)) {
204
            throw new InvalidArgumentException("Provided key [{$key}] is not valid variable name.");
205
        }
206
207
        $this->attributes['meta_key'] = $key;
208
    }
209
210
    /**
211
     * Set group of the meta attribute.
212
     *
213
     * @param string $group
214
     *
215
     * @throws \InvalidArgumentException
216
     */
217
    public function setMetaGroup($group = null)
218
    {
219
        if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $group) && $group !== null) {
220
            throw new InvalidArgumentException("Provided group [{$group}] is not valid variable name.");
221
        }
222
223
        $this->attributes['meta_group'] = $group;
224
    }
225
226
    /**
227
     * Set type of the meta attribute.
228
     *
229
     * @param mixed $value
230
     */
231
    protected function setType($value)
232
    {
233
        $this->attributes['meta_type'] = $this->hasMutator($value, 'setter')
234
            ? $this->getMutatedType($value, 'setter')
235
            : $this->getValueType($value);
236
    }
237
238
    /**
239
     * Set value of the meta attribute.
240
     *
241
     * @param mixed $value
242
     *
243
     * @throws \Sofa\Eloquence\Metable\InvalidTypeException
244
     */
245
    public function setValue($value)
246
    {
247
        $this->setType($value);
248
249
        if ($this->hasMutator($value, 'setter')) {
250
            $value = $this->mutateValue($value, 'setter');
251
        } elseif (!$this->isStringable($value) && !is_null($value)) {
252
            throw new InvalidTypeException(
253
                "Unsupported meta value type [{$this->getValueType($value)}]."
254
            );
255
        }
256
257
        $this->attributes['meta_value'] = $value;
258
    }
259
260
    /**
261
     * Mutate attribute value.
262
     *
263
     * @param  mixed  $value
264
     * @param  string $dir
265
     * @return mixed
266
     */
267
    protected function mutateValue($value, $dir = 'setter')
268
    {
269
        $mutator = $this->getMutator($value, $dir, $this->attributes['meta_type']);
270
271
        if (method_exists($this, $mutator)) {
272
            return $this->{$mutator}($value);
273
        }
274
275
        return static::$attributeMutator->mutate($value, $mutator);
276
    }
277
278
    /**
279
     * Determine whether the value type can be set to string.
280
     *
281
     * @param  mixed   $value
282
     * @return bool
283
     */
284
    protected function isStringable($value)
285
    {
286
        return is_scalar($value);
287
    }
288
289
    /**
290
     * Get the value type.
291
     *
292
     * @param  mixed  $value
293
     * @return string
294
     */
295
    protected function getValueType($value)
296
    {
297
        $type = is_object($value) ? get_class($value) : gettype($value);
298
299
        // use float instead of deprecated double
300
        return ($type == 'double') ? 'float' : $type;
301
    }
302
303
    /**
304
     * Get the mutated type.
305
     *
306
     * @param  mixed  $value
307
     * @param  string $dir
308
     * @return string
309
     */
310
    protected function getMutatedType($value, $dir = 'setter')
311
    {
312 View Code Duplication
        foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) {
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...
313
            if ($this->getValueType($value) == $mutated || $value instanceof $mutated) {
314
                return $mutated;
315
            }
316
        }
317
    }
318
319
    /**
320
     * Determine whether a mutator exists for the value type.
321
     *
322
     * @param  mixed   $value
323
     * @param  string  $dir
324
     * @return bool
325
     */
326
    protected function hasMutator($value, $dir = 'setter', $type = null)
327
    {
328
        return (bool) $this->getMutator($value, $dir, $type);
329
    }
330
331
    /**
332
     * Get mutator for the type.
333
     *
334
     * @param  mixed  $value
335
     * @param  string $dir
336
     * @return string
337
     */
338
    protected function getMutator($value, $dir = 'setter', $type = null)
339
    {
340
        $type = $type ?: $this->getValueType($value);
341
342 View Code Duplication
        foreach ($this->{"{$dir}Mutators"} as $mutated => $mutator) {
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...
343
            if ($type == $mutated || $value instanceof $mutated) {
344
                return $mutator;
345
            }
346
        }
347
    }
348
349
    /**
350
     * Allow custom table name for meta attributes via config.
351
     *
352
     * @return string
353
     */
354
    public function getTable()
355
    {
356
        return isset(static::$customTable) ? static::$customTable : parent::getTable();
357
    }
358
359
    /**
360
     * Set custom table for the meta attributes. Allows doing it only once
361
     * in order to mimic protected behaviour, most likely in the service
362
     * provider, which in turn gets the table name from configuration.
363
     *
364
     * @param string $table
365
     */
366
    public static function setCustomTable($table)
367
    {
368
        if (!isset(static::$customTable)) {
369
            static::$customTable = $table;
370
        }
371
    }
372
373
    /**
374
     * Handle casting value to string.
375
     *
376
     * @return string
377
     */
378
    public function castToString()
379
    {
380
        if ($this->attributes['meta_type'] == 'array') {
381
            return $this->attributes['meta_value'];
382
        }
383
384
        $value = $this->getValue();
385
386
        if ($this->isStringable($value) || is_object($value) && method_exists($value, '__toString')) {
387
            return (string) $value;
388
        }
389
390
        return '';
391
    }
392
393
    /**
394
     * Handle dynamic casting to string.
395
     *
396
     * @return string
397
     */
398
    public function __toString()
399
    {
400
        return $this->castToString();
401
    }
402
}
403