Passed
Push — traitrefactor ( dd3c0f...b1122e )
by Joe
03:48
created

HasArrayableAttributes::getAncestorProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace PhpWinTools\WmiScripting\Concerns;
4
5
use Exception;
6
use ReflectionClass;
7
use PhpWinTools\Support\StringModule;
8
use PhpWinTools\Support\BooleanModule;
9
use PhpWinTools\WmiScripting\Contracts\Arrayable;
10
use PhpWinTools\WmiScripting\Collections\ArrayCollection;
11
use function PhpWinTools\WmiScripting\Support\class_has_trait;
12
use Illuminate\Contracts\Support\Arrayable as IlluminateArrayable;
13
use function PhpWinTools\WmiScripting\Support\get_ancestor_property;
14
15
trait HasArrayableAttributes
16
{
17
    protected $casts_booted = false;
18
19
    protected $unmapped_attributes = [];
20
21
    protected $attribute_name_replacements = [];
22
23
    public function getAttribute($attribute, $default = null)
24
    {
25
        if ($this->hasAttributeMethod($attribute)) {
26
            return $this->getAttributeMethodValue('get' . StringModule::studly($attribute) . 'Attribute', $attribute);
27
        }
28
29
        $value = $default;
30
31
        if ($this->hasProperty($attribute)) {
32
            $value = $this->{$attribute};
33
        }
34
35
        if ($key = array_search($attribute, $this->attribute_name_replacements)) {
36
            $value = $this->{$key};
37
        }
38
39
        if (array_key_exists($attribute, $this->unmapped_attributes)) {
40
            $value = $this->unmapped_attributes[$attribute];
41
        }
42
43
        if ($this->hasCast($attribute)) {
44
            $value = $this->cast($attribute, $value);
45
        }
46
47
        return $value;
48
    }
49
50
    public function toArray(): array
51
    {
52
        return array_merge(
53
            $this->getCalculatedAttributes(),
54
            $this->iterateAttributes(get_class_vars(get_called_class())),
55
            $this->iterateAttributes($this->unmapped_attributes)
56
        );
57
    }
58
59
    public function collect(array $array)
60
    {
61
        return ArrayCollection::collect($array);
62
    }
63
64
    public function setUnmappedAttribute($key, $value)
65
    {
66
        $this->unmapped_attributes[$key] = $value;
67
68
        return $this;
69
    }
70
71
    public function getCasts(): array
72
    {
73
        if (!$this->casts_booted) {
74
            $this->bootCasts();
75
        }
76
77
        return $this->attribute_casting;
78
    }
79
80
    public function getCast($attribute)
81
    {
82
        $casts = $this->getCasts();
83
84
        return $casts[$attribute] ?? null;
85
    }
86
87
    public function hasCast($attribute): bool
88
    {
89
        return array_key_exists($attribute, $this->getCasts());
90
    }
91
92
    protected function bootCasts()
93
    {
94
        $merge_casting = true;
95
        $attribute_casting = [];
96
97
        if ($this->hasProperty('merge_parent_casting')) {
98
            $merge_casting = $this->merge_parent_casting;
99
        }
100
101
        if ($this->hasProperty('attribute_casting')) {
102
            $attribute_casting = $this->attribute_casting;
103
        }
104
105
        $this->attribute_casting = $merge_casting
0 ignored issues
show
Bug Best Practice introduced by
The property attribute_casting does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
106
            ? array_merge(get_ancestor_property(get_called_class(), 'attribute_casting'), $attribute_casting)
107
            : $attribute_casting;
108
109
        $this->casts_booted = true;
110
    }
111
112
    protected function getAttributeMethodValue($method, $attribute)
113
    {
114
        if ($this->hasProperty($attribute)) {
115
            return $this->{$method}($this->{$attribute});
116
        }
117
118
        if ($this->hasUnmappedAttribute($attribute)) {
119
            return  $this->{$method}($this->unmapped_attributes[$attribute]);
120
        }
121
122
        return $this->{$method}($attribute);
123
    }
124
125
    protected function getCalculatedAttributes()
126
    {
127
        return $this->collect($this->calculatedAttributes())->mapWithKeys(function ($attribute) {
128
            return [$attribute => $this->{'get' . lcfirst($attribute) . 'Attribute'}()];
129
        })->toArray();
130
    }
131
132
    protected function calculatedAttributes()
133
    {
134
        return $this->collect($this->getAttributeMethods())->map(function ($method) {
135
            return $this->getAttributeNameFromMethod($method);
136
        })->filter(function ($attribute) {
137
            return !$this->hasProperty($attribute) && !$this->hasUnmappedAttribute($attribute);
138
        })->values()->toArray();
139
    }
140
141
    protected function hasUnmappedAttribute($attribute)
142
    {
143
        return array_key_exists($attribute, $this->unmapped_attributes);
144
    }
145
146
    protected function hasAttributeMethod($attribute)
147
    {
148
        return $this->collect($this->getAttributeMethods())->filter(function ($method) use ($attribute) {
149
            if (($method_name = $this->getAttributeNameFromMethod($method)) === $attribute) {
150
                return true;
151
            }
152
153
            $method_name = StringModule::snake($method_name);
154
155
            if ($method_name === $attribute) {
156
                return true;
157
            }
158
159
            return false;
160
        })->isNotEmpty();
161
    }
162
163
    protected function getAttributeNameFromMethod($method)
164
    {
165
        return lcfirst(substr($method, 3, -9));
166
    }
167
168
    protected function getAttributeMethods()
169
    {
170
        return $this->collect(get_class_methods(get_called_class()))->filter(function ($method) {
171
            return substr($method, 0, 3) === 'get' && substr($method, -9) === 'Attribute' && $method !== 'getAttribute';
172
        })->values()->toArray();
173
    }
174
175
    public function hasProperty($property_name)
176
    {
177
        return array_key_exists($property_name, get_class_vars(get_called_class()));
178
    }
179
180
    protected function replaceAttributeName($key)
181
    {
182
        if (array_key_exists($key, $this->attribute_name_replacements)) {
183
            return $this->attribute_name_replacements[$key];
184
        }
185
186
        return $key;
187
    }
188
189
    protected function cast($key, $value)
190
    {
191
        $casts = $this->getCasts();
192
193
        if (!$this->hasCast($key)) {
194
            return $value;
195
        }
196
197
        switch ($casts[$key]) {
198
            case 'array':
199
                return is_array($value) ? $value : [$value];
200
            case 'bool':
201
            case 'boolean':
202
                return BooleanModule::makeBoolean($value);
203
            case 'int':
204
            case 'integer':
205
                // Prevent integer overflow
206
                return $value >= PHP_INT_MAX || $value <= PHP_INT_MIN ? (string) $value : (int) $value;
207
            case 'string':
208
                if ($value === true) {
209
                    return 'true';
210
                }
211
212
                if ($value === false) {
213
                    return 'false';
214
                }
215
216
                if (is_array($value)) {
217
                    return json_encode($value);
218
                }
219
220
                return (string) $value;
221
            default:
222
                return $value;
223
        }
224
    }
225
226
    protected function objectToArray($value)
227
    {
228
        if (!is_object($value)) {
229
            return $value;
230
        }
231
232
        $array = (array) $value;
233
234
        foreach ($array as $key => $value) {
0 ignored issues
show
introduced by
$value is overwriting one of the parameters of this function.
Loading history...
235
            if (strpos($key, "\x00*\x00") === 0) {
236
                $array[substr($key, 3)] = $value;
237
                unset($array[$key]);
238
            }
239
        }
240
241
        return $array;
242
    }
243
244
    protected function tryToArrayMethod($value)
245
    {
246
        if (!is_object($value)) {
247
            return $value;
248
        }
249
250
        if (is_object($value) && $this->hasToArrayMethod($value)) {
251
            try {
252
                $array = $value->toArray();
253
                if (is_array($array)) {
254
                    return $array;
255
                }
256
            } catch (Exception $exception) {
257
                return $value;
258
            }
259
        }
260
261
        return $value;
262
    }
263
264
    protected function hasToArrayMethod($value)
265
    {
266
        if (!is_object($value)) {
267
            return false;
268
        }
269
270
        if (in_array('toArray', get_class_methods($value))) {
271
            return true;
272
        }
273
274
        return false;
275
    }
276
277
    protected function transformToArray($value)
278
    {
279
        if (is_object($value) && ($value instanceof Arrayable || $value instanceof IlluminateArrayable)) {
280
            return $value->toArray();
281
        }
282
283
        if (is_object($value) && is_array(($array = $this->tryToArrayMethod($value)))) {
284
            return $array;
285
        }
286
287
        if (is_object($value)) {
288
            return $this->objectToArray($value);
289
        }
290
291
        if (is_array($value)) {
292
            return $this->iterateAttributes($value);
293
        }
294
295
        return $value;
296
    }
297
298
    protected function iterateAttributes(array $attributes)
299
    {
300
        $results = [];
301
302
        foreach ($attributes as $key => $value) {
303
            if (class_has_trait(get_called_class(), HasHiddenAttributes::class) && $this->isHidden($key)) {
0 ignored issues
show
Bug introduced by
It seems like isHidden() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

303
            if (class_has_trait(get_called_class(), HasHiddenAttributes::class) && $this->/** @scrutinizer ignore-call */ isHidden($key)) {
Loading history...
304
                continue;
305
            }
306
307
            $results[$this->replaceAttributeName($key)] = $this->transformToArray($this->getAttribute($key, $value));
308
        }
309
310
        return $results;
311
    }
312
}
313