Completed
Branch traitrefactor (c265e2)
by Joe
02:41
created

HasArrayableAttributes::getHiddenAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

306
            if ($this->/** @scrutinizer ignore-call */ isHidden($key)) {
Loading history...
307
                continue;
308
            }
309
310
            $results[$this->replaceAttributeName($key)] = $this->transformToArray($this->getAttribute($key, $value));
311
        }
312
313
        return $results;
314
    }
315
316
    protected function getAncestorProperty($property_name)
317
    {
318
        return $this->collect(class_parents($this))->map(function ($class) use ($property_name) {
319
            return (new ReflectionClass($class))->getDefaultProperties()[$property_name] ?? [];
320
        })->values()->collapse()->toArray();
321
    }
322
}
323