Passed
Push — master ( 93cd6a...e2ddf6 )
by Dominik
53s queued 11s
created

src/Denormalizer/Denormalizer.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 10
    public function __construct(
32
        DenormalizerObjectMappingRegistryInterface $denormalizerObjectMappingRegistry,
33
        LoggerInterface $logger = null
34
    ) {
35 10
        $this->denormalizerObjectMappingRegistry = $denormalizerObjectMappingRegistry;
36 10
        $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 10
    }
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 10
    public function denormalize($object, array $data, DenormalizerContextInterface $context = null, string $path = '')
51
    {
52 10
        $context = $context ?? DenormalizerContextBuilder::create()->getContext();
53
54 10
        $class = is_object($object) ? get_class($object) : $object;
55 10
        $objectMapping = $this->getObjectMapping($class);
56
57 9
        $type = null;
58 9
        if (isset($data['_type'])) {
59 1
            $type = $data['_type'];
60
61 1
            unset($data['_type']);
62
        }
63
64 9
        $isNew = false;
65 9
        if (!is_object($object)) {
66 7
            $isNew = true;
67 7
            $object = $this->createNewObject($objectMapping, $path, $type);
68
        }
69
70 8
        $missingFields = [];
71 8
        foreach ($objectMapping->getDenormalizationFieldMappings($path, $type) as $denormalizationFieldMapping) {
72 8
            $name = $denormalizationFieldMapping->getName();
73
74 8
            if (!array_key_exists($name, $data)) {
75 8
                $missingFields[] = $name;
76
77 8
                continue;
78
            }
79
80 6
            $this->denormalizeField($context, $denormalizationFieldMapping, $path, $name, $data, $object);
81
82 6
            unset($data[$name]);
83
        }
84
85 8
        $allowedAdditionalFields = $context->getAllowedAdditionalFields();
86
87 8
        if (null !== $allowedAdditionalFields
88 8
            && [] !== $fields = array_diff(array_keys($data), $allowedAdditionalFields)
89
        ) {
90 1
            $this->handleNotAllowedAdditionalFields($path, $fields);
91
        }
92
93 7
        if (!$isNew) {
94 2
            $this->resetMissingFields($context, $objectMapping, $object, $missingFields, $path, $type);
95
        }
96
97 7
        return $object;
98
    }
99
100
    /**
101
     * @param string $class
102
     *
103
     * @return DenormalizationObjectMappingInterface
104
     *
105
     * @throws DeserializerLogicException
106
     */
107 10
    private function getObjectMapping(string $class): DenormalizationObjectMappingInterface
108
    {
109
        try {
110 10
            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 7
    private function createNewObject(
126
        DenormalizationObjectMappingInterface $objectMapping,
127
        string $path,
128
        string $type = null
129
    ) {
130 7
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
131 7
        $object = $factory();
132
133 7
        if (is_object($object)) {
134 6
            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 6
    private function denormalizeField(
152
        DenormalizerContextInterface $context,
153
        DenormalizationFieldMappingInterface $denormalizationFieldMapping,
154
        string $path,
155
        string $name,
156
        array $data,
157
        $object
158
    ) {
159 6
        if (!$this->isWithinGroup($context, $denormalizationFieldMapping)) {
160 1
            return;
161
        }
162
163 5
        $subPath = $this->getSubPathByName($path, $name);
164
165 5
        $this->logger->info('deserialize: path {path}', ['path' => $subPath]);
166
167 5
        $fieldDenormalizer = $denormalizationFieldMapping->getFieldDenormalizer();
168 5
        $fieldDenormalizer->denormalizeField($subPath, $object, $data[$name], $context, $this);
169 5
    }
170
171
    /**
172
     * @param string $path
173
     * @param array  $names
174
     */
175 1
    private function handleNotAllowedAdditionalFields(string $path, array $names)
176
    {
177 1
        $exception = DeserializerRuntimeException::createNotAllowedAdditionalFields(
178 1
            $this->getSubPathsByNames($path, $names)
179
        );
180
181 1
        $this->logger->notice('deserialize: {exception}', ['exception' => $exception->getMessage()]);
182
183 1
        throw $exception;
184
    }
185
186
    /**
187
     * @param DenormalizerContextInterface         $context
188
     * @param DenormalizationFieldMappingInterface $fieldMapping
189
     *
190
     * @return bool
191
     */
192 6
    private function isWithinGroup(
193
        DenormalizerContextInterface $context,
194
        DenormalizationFieldMappingInterface $fieldMapping
195
    ): bool {
196 6
        if ([] === $groups = $context->getGroups()) {
197 4
            return true;
198
        }
199
200 2
        foreach ($fieldMapping->getGroups() as $group) {
201 1
            if (in_array($group, $groups, true)) {
202 1
                return true;
203
            }
204
        }
205
206 1
        return false;
207
    }
208
209
    /**
210
     * @param string     $path
211
     * @param string|int $name
212
     *
213
     * @return string
214
     */
215 6
    private function getSubPathByName(string $path, $name): string
216
    {
217 6
        return '' === $path ? (string) $name : $path.'.'.$name;
218
    }
219
220
    /**
221
     * @param string $path
222
     * @param array  $names
223
     *
224
     * @return array
225
     */
226 1
    private function getSubPathsByNames(string $path, array $names): array
227
    {
228 1
        $subPaths = [];
229 1
        foreach ($names as $name) {
230 1
            $subPaths[] = $this->getSubPathByName($path, $name);
231
        }
232
233 1
        return $subPaths;
234
    }
235
236
    /**
237
     * @param DenormalizerContextInterface          $context
238
     * @param DenormalizationObjectMappingInterface $objectMapping
239
     * @param object                                $object
240
     * @param array                                 $missingFields
241
     * @param string                                $path
242
     * @param string|null                           $type
243
     */
244 2
    private function resetMissingFields(
245
        DenormalizerContextInterface $context,
246
        DenormalizationObjectMappingInterface $objectMapping,
247
        $object,
248
        array $missingFields,
249
        string $path,
250
        string $type = null
251
    ) {
252 2
        if (!method_exists($context, 'isResetMissingFields') || !$context->isResetMissingFields()) {
253 1
            return;
254
        }
255
256 1
        $factory = $objectMapping->getDenormalizationFactory($path, $type);
257
258 1
        $newObject = $factory();
259
260 1
        foreach ($missingFields as $missingField) {
261 1
            $accessor = new PropertyAccessor($missingField);
262 1
            $accessor->setValue($object, $accessor->getValue($newObject));
263
        }
264 1
    }
265
}
266