Passed
Pull Request — master (#22)
by Dominik
02:24
created

Denormalizer::__construct()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.3471

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 12
cts 22
cp 0.5455
rs 8.9848
c 0
b 0
f 0
cc 5
nc 4
nop 2
crap 7.3471
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Deserialization\Denormalizer;
6
7
use Chubbyphp\Deserialization\DeserializerLogicException;
8
use Chubbyphp\Deserialization\DeserializerRuntimeException;
9
use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMappingInterface;
10
use Chubbyphp\Deserialization\Mapping\DenormalizationObjectMappingInterface;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\NullLogger;
13
14
final class Denormalizer implements DenormalizerInterface
15
{
16
    /**
17
     * @var DenormalizerObjectMappingRegistryInterface
18
     */
19
    private $denormalizerObjectMappingRegistry;
20
21
    /**
22
     * @var DenormalizationObjectMappingInterface[]
23
     */
24
    private $objectMappings = [];
25
26
    /**
27
     * @var LoggerInterface
28
     */
29
    private $logger;
30
31
    /**
32
     * @param DenormalizationObjectMappingInterface[]|DenormalizerObjectMappingRegistryInterface $objectMappings
33
     * @param LoggerInterface|null                                                               $logger
34
     */
35 11
    public function __construct(
36
        $objectMappings,
37
        LoggerInterface $logger = null
38
    ) {
39 11
        $this->logger = $logger ?? new NullLogger();
40
41 11
        if (is_array($objectMappings)) {
42
            foreach ($objectMappings as $objectMapping) {
43
                $this->addObjectMapping($objectMapping);
44
            }
45
46
            return;
47
        }
48
49 11
        if ($objectMappings instanceof DenormalizerObjectMappingRegistryInterface) {
50 11
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
51 11
                sprintf(
52 11
                    'Use "%s" instead of "%s" as __construct argument',
53 11
                    DenormalizationObjectMappingInterface::class.'[]',
54 11
                    DenormalizerObjectMappingRegistryInterface::class
55
                ),
56 11
                E_USER_DEPRECATED
57
            );
58
59 11
            $this->denormalizerObjectMappingRegistry = $objectMappings;
60
61 11
            return;
62
        }
63
64
        throw new \TypeError(
65
            sprintf(
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('%s::__construct...ttype($objectMappings)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
66
                '%s::__construct() expects parameter 1 to be %s|%s, %s given',
67
                self::class,
68
                DenormalizationObjectMappingInterface::class.'[]',
69
                DenormalizerObjectMappingRegistryInterface::class,
70
                is_object($objectMappings) ? get_class($objectMappings) : gettype($objectMappings)
71
            )
72
        );
73
    }
74
75
    /**
76
     * @param DenormalizationObjectMappingInterface $objectMapping
77
     */
78
    private function addObjectMapping(DenormalizationObjectMappingInterface $objectMapping)
79
    {
80
        $this->objectMappings[$objectMapping->getClass()] = $objectMapping;
81
    }
82
83
    /**
84
     * @param object|string                     $object
85
     * @param array                             $data
86
     * @param DenormalizerContextInterface|null $context
87
     * @param string                            $path
88
     *
89
     * @return object
90
     *
91
     * @throws DeserializerLogicException
92
     * @throws DeserializerRuntimeException
93
     */
94 11
    public function denormalize($object, array $data, DenormalizerContextInterface $context = null, string $path = '')
95
    {
96 11
        $context = $context ?? DenormalizerContextBuilder::create()->getContext();
97
98 11
        $class = is_object($object) ? get_class($object) : $object;
99 11
        $objectMapping = $this->getObjectMapping($class);
100
101 10
        $type = null;
102 10
        if (isset($data['_type'])) {
103 2
            $type = $data['_type'];
104
105 2
            unset($data['_type']);
106
        }
107
108 10
        if (!is_object($object)) {
109 9
            $object = $this->createNewObject($objectMapping, $path, $type);
110
        }
111
112 9
        foreach ($objectMapping->getDenormalizationFieldMappings($path, $type) as $denormalizationFieldMapping) {
113 9
            $this->denormalizeField($context, $denormalizationFieldMapping, $path, $data, $object);
114
115 9
            unset($data[$denormalizationFieldMapping->getName()]);
116
        }
117
118 9
        if (null !== $context->getAllowedAdditionalFields()
119 9
            && [] !== $fields = array_diff(array_keys($data), $context->getAllowedAdditionalFields())
120
        ) {
121 2
            $this->handleNotAllowedAdditionalFields($path, $fields);
122
        }
123
124 7
        return $object;
125
    }
126
127
    /**
128
     * @param string $class
129
     *
130
     * @return DenormalizationObjectMappingInterface
131
     *
132
     * @throws DeserializerLogicException
133
     */
134 11
    private function getObjectMapping(string $class): DenormalizationObjectMappingInterface
135
    {
136 11
        if (null !== $this->denormalizerObjectMappingRegistry) {
137
            try {
138 11
                return $this->denormalizerObjectMappingRegistry->getObjectMapping($class);
139 1
            } catch (DeserializerLogicException $exception) {
140 1
                $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
141
142 1
                throw $exception;
143
            }
144
        }
145
146
        $reflectionClass = new \ReflectionClass($class);
147
148
        if (in_array('Doctrine\Common\Persistence\Proxy', $reflectionClass->getInterfaceNames(), true)) {
149
            $class = $reflectionClass->getParentClass()->name;
150
        }
151
152
        if (isset($this->objectMappings[$class])) {
153
            return $this->objectMappings[$class];
154
        }
155
156
        $exception = DeserializerLogicException::createMissingMapping($class);
157
158
        $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
159
160
        throw $exception;
161
    }
162
163
    /**
164
     * @param DenormalizationObjectMappingInterface $objectMapping
165
     * @param string                                $path
166
     * @param string|null                           $type
167
     *
168
     * @return object
169
     */
170 9
    private function createNewObject(
171
        DenormalizationObjectMappingInterface $objectMapping,
172
        string $path,
173
        string $type = null
174
    ) {
175 9
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
176 9
        $object = $factory();
177
178 9
        if (is_object($object)) {
179 8
            return $object;
180
        }
181
182 1
        $exception = DeserializerLogicException::createFactoryDoesNotReturnObject($path, gettype($object));
183
184 1
        $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
185
186 1
        throw $exception;
187
    }
188
189
    /**
190
     * @param DenormalizerContextInterface         $context
191
     * @param DenormalizationFieldMappingInterface $denormalizationFieldMapping
192
     * @param string                               $path
193
     * @param array                                $data
194
     * @param object                               $object
195
     */
196 9
    private function denormalizeField(
197
        DenormalizerContextInterface $context,
198
        DenormalizationFieldMappingInterface $denormalizationFieldMapping,
199
        string $path,
200
        array $data,
201
        $object
202
    ) {
203 9
        $name = $denormalizationFieldMapping->getName();
204 9
        if (!array_key_exists($name, $data)) {
205 2
            return;
206
        }
207
208 7
        $fieldDenormalizer = $denormalizationFieldMapping->getFieldDenormalizer();
209
210 7
        if (!$this->isWithinGroup($context, $denormalizationFieldMapping)) {
211 1
            return;
212
        }
213
214 6
        $subPath = $this->getSubPathByName($path, $name);
215
216 6
        $this->logger->info('deserialize: path {path}', ['path' => $subPath]);
217
218 6
        $fieldDenormalizer->denormalizeField($subPath, $object, $data[$name], $context, $this);
219 6
    }
220
221
    /**
222
     * @param string $path
223
     * @param array  $names
224
     */
225 2
    private function handleNotAllowedAdditionalFields(string $path, array $names)
226
    {
227 2
        $exception = DeserializerRuntimeException::createNotAllowedAdditionalFields(
228 2
            $this->getSubPathsByNames($path, $names)
229
        );
230
231 2
        $this->logger->notice('deserialize: {exception}', ['exception' => $exception->getMessage()]);
232
233 2
        throw $exception;
234
    }
235
236
    /**
237
     * @param DenormalizerContextInterface         $context
238
     * @param DenormalizationFieldMappingInterface $fieldMapping
239
     *
240
     * @return bool
241
     */
242 7
    private function isWithinGroup(
243
        DenormalizerContextInterface $context,
244
        DenormalizationFieldMappingInterface $fieldMapping
245
    ): bool {
246 7
        if ([] === $groups = $context->getGroups()) {
247 5
            return true;
248
        }
249
250 2
        foreach ($fieldMapping->getGroups() as $group) {
251 1
            if (in_array($group, $groups, true)) {
252 1
                return true;
253
            }
254
        }
255
256 1
        return false;
257
    }
258
259
    /**
260
     * @param string     $path
261
     * @param string|int $name
262
     *
263
     * @return string
264
     */
265 7
    private function getSubPathByName(string $path, $name): string
266
    {
267 7
        return '' === $path ? (string) $name : $path.'.'.$name;
268
    }
269
270
    /**
271
     * @param string $path
272
     * @param array  $names
273
     *
274
     * @return array
275
     */
276 2
    private function getSubPathsByNames(string $path, array $names): array
277
    {
278 2
        $subPaths = [];
279 2
        foreach ($names as $name) {
280 2
            $subPaths[] = $this->getSubPathByName($path, $name);
281
        }
282
283 2
        return $subPaths;
284
    }
285
}
286