Completed
Pull Request — master (#29)
by Dominik
02:18
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 13
    public function __construct(
32
        DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry,
33
        LoggerInterface $logger = null
34
    ) {
35 13
        $this->denormalizerObjectMappingRegistry = $denormalizerObjectMappingRegistry;
36 13
        $this->logger = $logger ?? new NullLogger();
0 ignored issues
show
Documentation Bug introduced by
It seems like $logger ?? new \Psr\Log\NullLogger() can also be of type object<Psr\Log\NullLogger>. However, the property $logger is declared as type object<Psr\Log\LoggerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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