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