Completed
Push — master ( b5bb50...df2fdb )
by John
03:31
created

ObjectHydrator::hydrateNode()   C

Complexity

Conditions 17
Paths 17

Size

Total Lines 66
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 66
rs 5.8371
c 0
b 0
f 0
cc 17
eloc 44
nc 17
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of the KleijnWeb\PhpApi\Hydrator package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace KleijnWeb\PhpApi\Hydrator;
10
11
use KleijnWeb\PhpApi\Descriptions\Description\Schema\AnySchema;
12
use KleijnWeb\PhpApi\Descriptions\Description\Schema\ArraySchema;
13
use KleijnWeb\PhpApi\Descriptions\Description\Schema\ObjectSchema;
14
use KleijnWeb\PhpApi\Descriptions\Description\Schema\ScalarSchema;
15
use KleijnWeb\PhpApi\Descriptions\Description\Schema\Schema;
16
use KleijnWeb\PhpApi\Hydrator\Exception\UnsupportedException;
17
18
/**
19
 * @author John Kleijn <[email protected]>
20
 */
21
class ObjectHydrator implements Hydrator
22
{
23
    /**
24
     * @var AnySchema
25
     */
26
    private $anySchema;
27
28
    /**
29
     * @var bool
30
     */
31
    private $is32Bit;
32
33
    /**
34
     * @var DateTimeSerializer
35
     */
36
    private $dateTimeSerializer;
37
38
    /**
39
     * @var ClassNameResolver
40
     */
41
    private $classNameResolver;
42
43
    /**
44
     * ObjectHydrator constructor.
45
     *
46
     * @param ClassNameResolver  $classNameResolver
47
     * @param DateTimeSerializer $dateTimeSerializer
48
     * @param bool               $is32Bit
49
     */
50
    public function __construct(
51
        ClassNameResolver $classNameResolver,
52
        DateTimeSerializer $dateTimeSerializer = null,
53
        $is32Bit = null
54
    ) {
55
        $this->anySchema          = new AnySchema();
56
        $this->is32Bit            = $is32Bit !== null ? $is32Bit : PHP_INT_SIZE === 4;
57
        $this->dateTimeSerializer = $dateTimeSerializer ?: new DateTimeSerializer();
58
        $this->classNameResolver  = $classNameResolver;
59
    }
60
61
    /**
62
     * @param mixed  $data
63
     * @param Schema $schema
64
     *
65
     * @return mixed
66
     */
67
    public function hydrate($data, Schema $schema)
68
    {
69
        return $this->hydrateNode($data, $schema);
70
    }
71
72
    /**
73
     * @param mixed  $data
74
     * @param Schema $schema
75
     *
76
     * @return mixed
77
     */
78
    public function dehydrate($data, Schema $schema)
79
    {
80
        return $this->dehydrateNode($data, $schema);
81
    }
82
83
    /**
84
     * @param mixed  $node
85
     * @param Schema $schema
86
     *
87
     * @return mixed
88
     */
89
    private function hydrateNode($node, Schema $schema)
90
    {
91
        $node = $this->applyDefaults($node, $schema);
92
93
        if ($schema instanceof AnySchema) {
94
            if (is_array($node)) {
95
                return array_map(function ($value) use ($schema) {
96
                    return $this->hydrateNode($value, $this->anySchema);
97
                }, $node);
98
            } elseif (is_object($node)) {
99
                $object = (object)[];
100
                foreach ($node as $property => $value) {
101
                    $object->$property = $this->hydrateNode($value, $this->anySchema);
102
                }
103
104
                return $object;
105
            }
106
            if (is_numeric($node)) {
107
                return ctype_digit($node) ? (int)$node : (float)$node;
108
            }
109
            try {
110
                $node = $this->dateTimeSerializer->deserialize($node, $schema);
111
            } catch (\Throwable $e) {
112
                return $node;
113
            }
114
115
        } elseif ($schema instanceof ArraySchema) {
116
            return array_map(function ($value) use ($schema) {
117
                return $this->hydrateNode($value, $schema->getItemsSchema());
118
            }, $node);
119
        } elseif ($schema instanceof ObjectSchema) {
120
            if (!$schema->hasComplexType()) {
121
                $object = clone $node;
122
                foreach ($node as $name => $value) {
123
                    if ($schema->hasPropertySchema($name)) {
124
                        $object->$name = $this->hydrateNode($value, $schema->getPropertySchema($name));
125
                    }
126
                }
127
128
                return $object;
129
            }
130
            $fqcn = $this->classNameResolver->resolve($schema->getComplexType()->getName());;
131
            $object    = unserialize(sprintf('O:%d:"%s":0:{}', strlen($fqcn), $fqcn));
132
            $reflector = new \ReflectionObject($object);
133
134
            foreach ($node as $name => $value) {
135
                if (!$reflector->hasProperty($name)) {
136
                    continue;
137
                }
138
139
                if ($schema->hasPropertySchema($name)) {
140
                    $value = $this->hydrateNode($value, $schema->getPropertySchema($name));
141
                }
142
143
                $attribute = $reflector->getProperty($name);
144
                $attribute->setAccessible(true);
145
                $attribute->setValue($object, $value);
146
            }
147
148
            return $object;
149
        } elseif ($schema instanceof ScalarSchema) {
150
            return $this->castScalarValue($node, $schema);
151
        }
152
153
        return $node;
154
    }
155
156
    /**
157
     * @param mixed  $node
158
     * @param Schema $schema
159
     *
160
     * @return mixed
161
     */
162
    private function dehydrateNode($node, Schema $schema)
163
    {
164
        if ($node instanceof \DateTimeInterface) {
165
            $node = $this->dateTimeSerializer->serialize($node, $schema);
166
        } elseif ($this->shouldTreatAsArray($node, $schema)) {
167
            $node = array_map(function ($value) use ($schema) {
168
                return $this->dehydrateNode(
169
                    $value,
170
                    $schema instanceof ArraySchema ? $schema->getItemsSchema() : $this->anySchema
171
                );
172
            }, $node);
173
        } elseif ($this->shouldTreatAsObject($node, $schema)) {
174
            $input = $node;
175
            $node  = (object)[];
176
177
            if (!$input instanceof \stdClass) {
178
                $input = $this->extractValuesFromTypedObject($input);
179
            }
180
181
            foreach ($input as $name => $value) {
0 ignored issues
show
Bug introduced by
The expression $input of type array|object<stdClass> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
182
183
                $valueSchema = $schema instanceof ObjectSchema && $schema->hasPropertySchema($name)
184
                    ? $schema->getPropertySchema($name)
185
                    : $this->anySchema;
186
187
                if ($this->isAllowedNull($value, $valueSchema)) {
188
                    $node->$name = null;
189
                    continue;
190
                }
191
192
                if (null !== $value) {
193
                    $node->$name = $this->dehydrateNode(
194
                        $value,
195
                        $valueSchema
196
                    );
197
                }
198
            }
199
        }
200
201
        return $node;
202
    }
203
204
    /**
205
     * Cast a scalar value using the schema.
206
     *
207
     * @param mixed        $value
208
     * @param ScalarSchema $schema
209
     *
210
     * @return float|int|string|\DateTimeInterface
211
     * @throws UnsupportedException
212
     */
213
    private function castScalarValue($value, ScalarSchema $schema)
214
    {
215
        if ($schema->isType(Schema::TYPE_NUMBER)) {
216
            return ctype_digit($value) ? (int)$value : (float)$value;
217
        }
218
        if ($schema->isType(Schema::TYPE_INT)) {
219
            if ($this->is32Bit && $schema->hasFormat(Schema::FORMAT_INT64)) {
220
                throw new UnsupportedException("Operating system does not support 64 bit integers");
221
            }
222
223
            return (int)$value;
224
        }
225
        if ($schema->isDateTime()) {
226
            return $this->dateTimeSerializer->deserialize($value, $schema);
227
        }
228
229
        settype($value, $schema->getType());
230
231
        return $value;
232
    }
233
234
    /**
235
     * @param mixed  $node
236
     * @param Schema $schema
237
     *
238
     * @return bool
239
     */
240
    private function shouldTreatAsObject($node, Schema $schema): bool
241
    {
242
        return $schema instanceof ObjectSchema || $schema instanceof AnySchema && is_object($node);
243
    }
244
245
    /**
246
     * @param mixed  $node
247
     * @param Schema $schema
248
     *
249
     * @return bool
250
     */
251
    private function shouldTreatAsArray($node, Schema $schema): bool
252
    {
253
        return $schema instanceof ArraySchema || $schema instanceof AnySchema && is_array($node);
254
    }
255
256
    /**
257
     * @param mixed  $value
258
     * @param Schema $schema
259
     * @return bool
260
     */
261
    private function isAllowedNull($value, Schema $schema): bool
262
    {
263
        return $value === null && $schema instanceof ScalarSchema && $schema->isType(Schema::TYPE_NULL);
264
    }
265
266
    /**
267
     * @param mixed  $node
268
     * @param Schema $schema
269
     *
270
     * @return mixed
271
     */
272
    private function applyDefaults($node, Schema $schema)
273
    {
274
        if ($node instanceof \stdClass && $schema instanceof ObjectSchema) {
275
            /** @var Schema $propertySchema */
276
            foreach ($schema->getPropertySchemas() as $name => $propertySchema) {
277
                if (!isset($node->$name) && null !== $default = $propertySchema->getDefault()) {
278
                    $node->$name = $default;
279
                }
280
            }
281
        }
282
283
        return $node === null ? $schema->getDefault() : $node;
284
    }
285
286
    /**
287
     * @param $node
288
     * @return \stdClass
289
     */
290
    private function extractValuesFromTypedObject($node): array
291
    {
292
        $reflector  = new \ReflectionObject($node);
293
        $properties = $reflector->getProperties();
294
        $data       = [];
295
        foreach ($properties as $attribute) {
296
            $attribute->setAccessible(true);
297
            $data[$attribute->getName()] = $attribute->getValue($node);
298
        }
299
300
        return $data;
301
    }
302
}
303