Completed
Pull Request — master (#29)
by Dominik
06:01
created

Denormalizer::isCompliant()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 4
cts 4
cp 1
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Deserialization\Denormalizer;
6
7
use Chubbyphp\Deserialization\Accessor\PropertyAccessor;
8
use Chubbyphp\Deserialization\DeserializerLogicException;
9
use Chubbyphp\Deserialization\DeserializerRuntimeException;
10
use Chubbyphp\Deserialization\Mapping\DenormalizationFieldMappingInterface;
11
use Chubbyphp\Deserialization\Mapping\DenormalizationObjectMappingInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\NullLogger;
14
15
final class Denormalizer implements DenormalizerInterface
16
{
17
    /**
18
     * @var DenormalizerObjectMappingRegistryInterface
19
     */
20
    private $denormalizerObjectMappingRegistry;
21
22
    /**
23
     * @var LoggerInterface
24
     */
25
    private $logger;
26
27
    /**
28
     * @param DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry
29
     * @param LoggerInterface|null                       $logger
30
     */
31 14
    public function __construct(
32
        DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry,
33
        LoggerInterface $logger = null
34
    ) {
35 14
        $this->denormalizerObjectMappingRegistry = $denormalizerObjectMappingRegistry;
36 14
        $this->logger = $logger ?? new NullLogger();
37 14
    }
38
39
    /**
40
     * @param object|string                     $object
41
     * @param array                             $data
42
     * @param DenormalizerContextInterface|null $context
43
     * @param string                            $path
44
     *
45
     * @return object
46
     *
47
     * @throws DeserializerLogicException
48
     * @throws DeserializerRuntimeException
49
     */
50 14
    public function denormalize($object, array $data, DenormalizerContextInterface $context = null, string $path = '')
51
    {
52 14
        $context = $context ?? DenormalizerContextBuilder::create()->getContext();
53
54 14
        $class = is_object($object) ? get_class($object) : $object;
55 14
        $objectMapping = $this->getObjectMapping($class);
56
57 13
        $type = null;
58 13
        if (isset($data['_type'])) {
59 2
            $type = $data['_type'];
60
61 2
            unset($data['_type']);
62
        }
63
64 13
        $isNew = false;
65 13
        if (!is_object($object)) {
66 10
            $isNew = true;
67 10
            $object = $this->createNewObject($objectMapping, $path, $type);
68
        }
69
70 12
        $missingFields = [];
71 12
        $additionalFields = array_flip(array_keys($data));
72 12
        foreach ($objectMapping->getDenormalizationFieldMappings($path, $type) as $denormalizationFieldMapping) {
73 12
            $name = $denormalizationFieldMapping->getName();
74
75 12
            if (!array_key_exists($name, $data)) {
76 11
                $missingFields[] = $name;
77
78 11
                continue;
79
            }
80
81 10
            $this->denormalizeField($context, $denormalizationFieldMapping, $path, $name, $data, $object);
82
83 10
            unset($additionalFields[$name]);
84
        }
85
86 12
        $allowedAdditionalFields = $context->getAllowedAdditionalFields();
87
88 12
        if (null !== $allowedAdditionalFields
89 12
            && [] !== $fields = array_diff(array_keys($additionalFields), $allowedAdditionalFields)
90
        ) {
91 1
            $this->handleNotAllowedAdditionalFields($path, $fields);
92
        }
93
94 11
        if (!$isNew) {
95 3
            $this->resetMissingFields($context, $objectMapping, $object, $missingFields, $path, $type);
96
        }
97
98 11
        return $object;
99
    }
100
101
    /**
102
     * @param string $class
103
     *
104
     * @return DenormalizationObjectMappingInterface
105
     *
106
     * @throws DeserializerLogicException
107
     */
108 14
    private function getObjectMapping(string $class): DenormalizationObjectMappingInterface
109
    {
110
        try {
111 14
            return $this->denormalizerObjectMappingRegistry->getObjectMapping($class);
112 1
        } catch (DeserializerLogicException $exception) {
113 1
            $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
114
115 1
            throw $exception;
116
        }
117
    }
118
119
    /**
120
     * @param DenormalizationObjectMappingInterface $objectMapping
121
     * @param string                                $path
122
     * @param string|null                           $type
123
     *
124
     * @return object
125
     */
126 10
    private function createNewObject(
127
        DenormalizationObjectMappingInterface $objectMapping,
128
        string $path,
129
        string $type = null
130
    ) {
131 10
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
132 10
        $object = $factory();
133
134 10
        if (is_object($object)) {
135 9
            return $object;
136
        }
137
138 1
        $exception = DeserializerLogicException::createFactoryDoesNotReturnObject($path, gettype($object));
139
140 1
        $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
141
142 1
        throw $exception;
143
    }
144
145
    /**
146
     * @param DenormalizerContextInterface         $context
147
     * @param DenormalizationFieldMappingInterface $denormalizationFieldMapping
148
     * @param string                               $path
149
     * @param array                                $data
150
     * @param object                               $object
151
     */
152 10
    private function denormalizeField(
153
        DenormalizerContextInterface $context,
154
        DenormalizationFieldMappingInterface $denormalizationFieldMapping,
155
        string $path,
156
        string $name,
157
        array $data,
158
        $object
159
    ) {
160 10
        if (!$this->isCompliant($context, $denormalizationFieldMapping, $object)) {
161 1
            return;
162
        }
163
164 9
        if (!$this->isWithinGroup($context, $denormalizationFieldMapping)) {
165 1
            return;
166
        }
167
168 8
        $subPath = $this->getSubPathByName($path, $name);
169
170 8
        $this->logger->info('deserialize: path {path}', ['path' => $subPath]);
171
172 8
        $fieldDenormalizer = $denormalizationFieldMapping->getFieldDenormalizer();
173 8
        $fieldDenormalizer->denormalizeField($subPath, $object, $data[$name], $context, $this);
174 8
    }
175
176
    /**
177
     * @param string $path
178
     * @param array  $names
179
     */
180 1
    private function handleNotAllowedAdditionalFields(string $path, array $names)
181
    {
182 1
        $exception = DeserializerRuntimeException::createNotAllowedAdditionalFields(
183 1
            $this->getSubPathsByNames($path, $names)
184
        );
185
186 1
        $this->logger->notice('deserialize: {exception}', ['exception' => $exception->getMessage()]);
187
188 1
        throw $exception;
189
    }
190
191
    /**
192
     * @param DenormalizerContextInterface         $context
193
     * @param DenormalizationFieldMappingInterface $mapping
194
     * @param object                               $object
195
     *
196
     * @return bool
197
     */
198 10
    private function isCompliant(
199
        DenormalizerContextInterface $context,
200
        DenormalizationFieldMappingInterface $mapping,
201
        $object
202
    ): bool {
203 10
        if (!is_callable([$mapping, 'getPolicy'])) {
204 8
            return true;
205
        }
206
207 2
        return $mapping->getPolicy()->isCompliant($context, $object);
208
    }
209
210
    /**
211
     * @param DenormalizerContextInterface         $context
212
     * @param DenormalizationFieldMappingInterface $fieldMapping
213
     *
214
     * @return bool
215
     */
216 9
    private function isWithinGroup(
217
        DenormalizerContextInterface $context,
218
        DenormalizationFieldMappingInterface $fieldMapping
219
    ): bool {
220 9
        if ([] === $groups = $context->getGroups()) {
0 ignored issues
show
Deprecated Code introduced by
The method Chubbyphp\Deserializatio...tInterface::getGroups() has been deprecated.

This method has been deprecated.

Loading history...
221 7
            return true;
222
        }
223
224 2
        foreach ($fieldMapping->getGroups() as $group) {
0 ignored issues
show
Deprecated Code introduced by
The method Chubbyphp\Deserializatio...gInterface::getGroups() has been deprecated.

This method has been deprecated.

Loading history...
225 1
            if (in_array($group, $groups, true)) {
226 1
                return true;
227
            }
228
        }
229
230 1
        return false;
231
    }
232
233
    /**
234
     * @param string $path
235
     * @param string $name
236
     *
237
     * @return string
238
     */
239 9
    private function getSubPathByName(string $path, string $name): string
240
    {
241 9
        return '' === $path ? $name : $path.'.'.$name;
242
    }
243
244
    /**
245
     * @param string $path
246
     * @param array  $names
247
     *
248
     * @return array
249
     */
250 1
    private function getSubPathsByNames(string $path, array $names): array
251
    {
252 1
        $subPaths = [];
253 1
        foreach ($names as $name) {
254 1
            $subPaths[] = $this->getSubPathByName($path, $name);
255
        }
256
257 1
        return $subPaths;
258
    }
259
260
    /**
261
     * @param DenormalizerContextInterface          $context
262
     * @param DenormalizationObjectMappingInterface $objectMapping
263
     * @param object                                $object
264
     * @param array                                 $missingFields
265
     * @param string                                $path
266
     * @param string|null                           $type
267
     */
268 3
    private function resetMissingFields(
269
        DenormalizerContextInterface $context,
270
        DenormalizationObjectMappingInterface $objectMapping,
271
        $object,
272
        array $missingFields,
273
        string $path,
274
        string $type = null
275
    ) {
276 3
        if (!method_exists($context, 'isResetMissingFields') || !$context->isResetMissingFields()) {
277 2
            return;
278
        }
279
280 1
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
281
282 1
        $newObject = $factory();
283
284 1
        foreach ($missingFields as $missingField) {
285 1
            $accessor = new PropertyAccessor($missingField);
286 1
            $accessor->setValue($object, $accessor->getValue($newObject));
287
        }
288 1
    }
289
}
290