Test Failed
Pull Request — master (#29)
by Dominik
01:54
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 Chubbyphp\Deserialization\Policy\GroupPolicy;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
16
final class Denormalizer implements DenormalizerInterface
17
{
18
    /**
19
     * @var DenormalizerObjectMappingRegistryInterface
20
     */
21
    private $denormalizerObjectMappingRegistry;
22
23
    /**
24
     * @var LoggerInterface
25
     */
26
    private $logger;
27
28
    /**
29
     * @param DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry
30
     * @param LoggerInterface|null                       $logger
31 12
     */
32
    public function __construct(
33
        DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry,
34
        LoggerInterface $logger = null
35 12
    ) {
36 12
        $this->denormalizerObjectMappingRegistry = $denormalizerObjectMappingRegistry;
37 12
        $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...
38
    }
39
40
    /**
41
     * @param object|string                     $object
42
     * @param array                             $data
43
     * @param DenormalizerContextInterface|null $context
44
     * @param string                            $path
45
     *
46
     * @return object
47
     *
48
     * @throws DeserializerLogicException
49
     * @throws DeserializerRuntimeException
50 12
     */
51
    public function denormalize($object, array $data, DenormalizerContextInterface $context = null, string $path = '')
52 12
    {
53
        $context = $context ?? DenormalizerContextBuilder::create()->getContext();
54 12
55 12
        $class = is_object($object) ? get_class($object) : $object;
56
        $objectMapping = $this->getObjectMapping($class);
57 11
58 11
        $type = null;
59 2
        if (isset($data['_type'])) {
60
            $type = $data['_type'];
61 2
62
            unset($data['_type']);
63
        }
64 11
65 11
        $isNew = false;
66 8
        if (!is_object($object)) {
67 8
            $isNew = true;
68
            $object = $this->createNewObject($objectMapping, $path, $type);
69
        }
70 10
71 10
        $missingFields = [];
72 10
        $additionalFields = array_flip(array_keys($data));
73 10
        foreach ($objectMapping->getDenormalizationFieldMappings($path, $type) as $denormalizationFieldMapping) {
74
            $name = $denormalizationFieldMapping->getName();
75 10
76 9
            if (!array_key_exists($name, $data)) {
77
                $missingFields[] = $name;
78 9
79
                continue;
80
            }
81 8
82
            $this->denormalizeField($context, $denormalizationFieldMapping, $path, $name, $data, $object);
83 8
84
            unset($additionalFields[$name]);
85
        }
86 10
87
        $allowedAdditionalFields = $context->getAllowedAdditionalFields();
88 10
89 10
        if (null !== $allowedAdditionalFields
90
            && [] !== $fields = array_diff(array_keys($additionalFields), $allowedAdditionalFields)
91 1
        ) {
92
            $this->handleNotAllowedAdditionalFields($path, $fields);
93
        }
94 9
95 3
        if (!$isNew) {
96
            $this->resetMissingFields($context, $objectMapping, $object, $missingFields, $path, $type);
97
        }
98 9
99
        return $object;
100
    }
101
102
    /**
103
     * @param string $class
104
     *
105
     * @return DenormalizationObjectMappingInterface
106
     *
107
     * @throws DeserializerLogicException
108 12
     */
109
    private function getObjectMapping(string $class): DenormalizationObjectMappingInterface
110
    {
111 12
        try {
112 1
            return $this->denormalizerObjectMappingRegistry->getObjectMapping($class);
113 1
        } catch (DeserializerLogicException $exception) {
114
            $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
115 1
116
            throw $exception;
117
        }
118
    }
119
120
    /**
121
     * @param DenormalizationObjectMappingInterface $objectMapping
122
     * @param string                                $path
123
     * @param string|null                           $type
124
     *
125
     * @return object
126 8
     */
127
    private function createNewObject(
128
        DenormalizationObjectMappingInterface $objectMapping,
129
        string $path,
130
        string $type = null
131 8
    ) {
132 8
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
133
        $object = $factory();
134 8
135 7
        if (is_object($object)) {
136
            return $object;
137
        }
138 1
139
        $exception = DeserializerLogicException::createFactoryDoesNotReturnObject($path, gettype($object));
140 1
141
        $this->logger->error('deserialize: {exception}', ['exception' => $exception->getMessage()]);
142 1
143
        throw $exception;
144
    }
145
146
    /**
147
     * @param DenormalizerContextInterface         $context
148
     * @param DenormalizationFieldMappingInterface $denormalizationFieldMapping
149
     * @param string                               $path
150
     * @param array                                $data
151
     * @param object                               $object
152 8
     */
153
    private function denormalizeField(
154
        DenormalizerContextInterface $context,
155
        DenormalizationFieldMappingInterface $denormalizationFieldMapping,
156
        string $path,
157
        string $name,
158
        array $data,
159
        $object
160 8
    ) {
161 1
        if (!$this->isCompliant($context, $denormalizationFieldMapping, $object)) {
162
            return;
163
        }
164 7
165
        if (!$this->isWithinGroup($context, $denormalizationFieldMapping)) {
166 7
            return;
167
        }
168 7
169 7
        $subPath = $this->getSubPathByName($path, $name);
170 7
171
        $this->logger->info('deserialize: path {path}', ['path' => $subPath]);
172
173
        $fieldDenormalizer = $denormalizationFieldMapping->getFieldDenormalizer();
174
        $fieldDenormalizer->denormalizeField($subPath, $object, $data[$name], $context, $this);
175
    }
176 1
177
    /**
178 1
     * @param string $path
179 1
     * @param array  $names
180
     */
181
    private function handleNotAllowedAdditionalFields(string $path, array $names)
182 1
    {
183
        $exception = DeserializerRuntimeException::createNotAllowedAdditionalFields(
184 1
            $this->getSubPathsByNames($path, $names)
185
        );
186
187
        $this->logger->notice('deserialize: {exception}', ['exception' => $exception->getMessage()]);
188
189
        throw $exception;
190
    }
191
192
    /**
193 8
     * @param DenormalizerContextInterface         $context
194
     * @param DenormalizationFieldMappingInterface $mapping
195
     * @param object                               $object
196
     *
197 8
     * @return bool
198 6
     */
199
    private function isCompliant(
200
        DenormalizerContextInterface $context,
201 2
        DenormalizationFieldMappingInterface $mapping,
202 1
        $object
203 1
    ): bool {
204
        if (!is_callable([$mapping, 'getPolicy'])) {
205
            return true;
206
        }
207 1
208
        return $mapping->getPolicy()->isCompliant($context, $object);
209
    }
210
211
    /**
212
     * @param DenormalizerContextInterface         $context
213
     * @param DenormalizationFieldMappingInterface $fieldMapping
214
     *
215
     * @return bool
216 8
     */
217
    private function isWithinGroup(
218 8
        DenormalizerContextInterface $context,
219
        DenormalizationFieldMappingInterface $fieldMapping
220
    ): bool {
221
        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...
222
            return true;
223
        }
224
225
        @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...
226
            sprintf(
227 1
                'Use "%s" instead of "%s::setGroups"',
228
                GroupPolicy::class,
229 1
                DenormalizerContextInterface::class
230 1
            ),
231 1
            E_USER_DEPRECATED
232
        );
233
234 1
        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...
235
            if (in_array($group, $groups, true)) {
236
                return true;
237
            }
238
        }
239
240
        return false;
241
    }
242
243
    /**
244
     * @param string $path
245 3
     * @param string $name
246
     *
247
     * @return string
248
     */
249
    private function getSubPathByName(string $path, string $name): string
250
    {
251
        return '' === $path ? $name : $path.'.'.$name;
252
    }
253 3
254 2
    /**
255
     * @param string $path
256
     * @param array  $names
257 1
     *
258
     * @return array
259 1
     */
260
    private function getSubPathsByNames(string $path, array $names): array
261 1
    {
262 1
        $subPaths = [];
263 1
        foreach ($names as $name) {
264
            $subPaths[] = $this->getSubPathByName($path, $name);
265 1
        }
266
267
        return $subPaths;
268
    }
269
270
    /**
271
     * @param DenormalizerContextInterface          $context
272
     * @param DenormalizationObjectMappingInterface $objectMapping
273
     * @param object                                $object
274
     * @param array                                 $missingFields
275
     * @param string                                $path
276
     * @param string|null                           $type
277
     */
278
    private function resetMissingFields(
279
        DenormalizerContextInterface $context,
280
        DenormalizationObjectMappingInterface $objectMapping,
281
        $object,
282
        array $missingFields,
283
        string $path,
284
        string $type = null
285
    ) {
286
        if (!method_exists($context, 'isResetMissingFields') || !$context->isResetMissingFields()) {
287
            return;
288
        }
289
290
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
291
292
        $newObject = $factory();
293
294
        foreach ($missingFields as $missingField) {
295
            $accessor = new PropertyAccessor($missingField);
296
            $accessor->setValue($object, $accessor->getValue($newObject));
297
        }
298
    }
299
}
300