Completed
Pull Request — master (#60)
by
unknown
01:56
created

ObjectNormalizer::denormalizeValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 3
nc 3
nop 4
1
<?php
2
3
/*
4
 * (c) Kévin Dunglas <[email protected]>
5
 *
6
 * This source file is subject to the MIT license that is bundled
7
 * with this source code in the file LICENSE.
8
 */
9
10
namespace Dunglas\DoctrineJsonOdm\Normalizer;
11
12
use Doctrine\Common\Util\ClassUtils;
13
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
14
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
15
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
16
use Symfony\Component\Serializer\SerializerAwareInterface;
17
use Symfony\Component\Serializer\SerializerInterface;
18
19
/**
20
 * Transforms an object to an array with the following keys:
21
 * * _type: the class name
22
 * * _value: a representation of the values of the object.
23
 *
24
 * @author Kévin Dunglas <[email protected]>
25
 */
26
final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
27
{
28
    // To not use the current normalizer
29
    const WONT_DENORMALIZE = 'wont_denormalize';
30
    const TYPE_FIELD = '#type';
31
32
    /**
33
     * @var NormalizerInterface|DenormalizerInterface
34
     */
35
    private $serializer;
36
    private $objectNormalizer;
37
38
    public function __construct(NormalizerInterface $objectNormalizer)
39
    {
40
        if (!$objectNormalizer instanceof DenormalizerInterface) {
41
            throw new \InvalidArgumentException(sprintf('The normalizer used must implement the "%s" interface.', DenormalizerInterface::class));
42
        }
43
44
        $this->objectNormalizer = $objectNormalizer;
45
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50
    public function normalize($object, $format = null, array $context = [])
51
    {
52
        return \array_merge([self::TYPE_FIELD => ClassUtils::getClass($object)], $this->objectNormalizer->normalize($object, $format, $context));
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function supportsNormalization($data, $format = null)
59
    {
60
        return \is_object($data);
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function denormalize($data, $class, $format = null, array $context = [])
67
    {
68
        if (!\is_iterable($data) || isset($context[self::WONT_DENORMALIZE]) || \is_object($data)) {
69
            return $data;
70
        }
71
72
        if (null !== $type = $this->extractType($data)) {
73
            return $this->denormalizeObject($data, $type, $format, $context);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 66 can also be of type string; however, Dunglas\DoctrineJsonOdm\...er::denormalizeObject() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
74
        }
75
76
        foreach ($data as $key => $value) {
77
            $data[$key] = $this->denormalizeValue($value, $class, $format, $context);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 66 can also be of type string; however, Dunglas\DoctrineJsonOdm\...zer::denormalizeValue() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
78
        }
79
80
        return $data;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function supportsDenormalization($data, $type, $format = null)
87
    {
88
        return true;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function setSerializer(SerializerInterface $serializer)
95
    {
96
        if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) {
97
            throw new \InvalidArgumentException(
98
                sprintf('The injected serializer must implement "%s" and "%s".', NormalizerInterface::class, DenormalizerInterface::class)
99
            );
100
        }
101
102
        $this->serializer = $serializer;
103
104
        if ($this->objectNormalizer instanceof SerializerAwareInterface) {
105
            $this->objectNormalizer->setSerializer($serializer);
106
        }
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function hasCacheableSupportsMethod(): bool
113
    {
114
        return true;
115
    }
116
117
    /**
118
     * Converts data to $class' object if possible.
119
     *
120
     * @param array  $data
121
     * @param string $class
122
     * @param null   $format
123
     * @param array  $context
124
     *
125
     * @return object|null
126
     */
127
    private function denormalizeObject(array $data, string $class, $format = null, array $context = [])
128
    {
129
        return $this->denormalizeObjectInOtherNormalizer($data, $class, $format, $context)
130
            ?? $this->denormalizeObjectInDefaultObjectNormalizer($data, $class, $format, $context);
131
    }
132
133
    /**
134
     * Tries to convert data to $class' object not using current normalizer.
135
     * This is useful if you have your own normalizers - they will have priority over this one.
136
     *
137
     * @param array  $data
138
     * @param string $class
139
     * @param null   $format
140
     * @param array  $context
141
     *
142
     * @return object|null
143
     */
144
    private function denormalizeObjectInOtherNormalizer(array $data, string $class, $format = null, array $context = [])
145
    {
146
        $context[self::WONT_DENORMALIZE] = true;
147
148
        return \is_object($object = $this->serializer->denormalize($data, $class, $format, $context)) ? $object : null;
0 ignored issues
show
Bug introduced by
The method denormalize does only exist in Symfony\Component\Serial...r\DenormalizerInterface, but not in Symfony\Component\Serial...zer\NormalizerInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
149
    }
150
151
    /**
152
     * Default denormalization $data to class using symfony's object normalizer.
153
     *
154
     * @param array  $data
155
     * @param string $class
156
     * @param null   $format
157
     * @param array  $context
158
     *
159
     * @return object
160
     */
161
    private function denormalizeObjectInDefaultObjectNormalizer(array $data, string $class, $format = null, array $context = [])
162
    {
163
        foreach ($data as $key => $value) {
164
            $data[$key] = $this->denormalizeValue($value, $class, $format, $context);
165
        }
166
167
        return $this->objectNormalizer->denormalize($data, $class, $format, $context);
168
    }
169
170
    /**
171
     * Convert raw value to normalized value - object or primitive type.
172
     *
173
     * @param mixed  $value
174
     * @param string $class
175
     * @param null   $format
176
     * @param array  $context
177
     *
178
     * @return object|null
179
     */
180
    private function denormalizeValue($value, string $class, $format = null, array $context = [])
181
    {
182
        if (\is_object($value)) {
183
            return $value;
184
        }
185
186
        return (null !== $type = $this->extractType($value))
187
            ? $this->denormalizeObject($value, $type, $format, $context)
188
            : $this->serializer->denormalize($value, $class, $format, $context);
0 ignored issues
show
Bug introduced by
The method denormalize does only exist in Symfony\Component\Serial...r\DenormalizerInterface, but not in Symfony\Component\Serial...zer\NormalizerInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
189
    }
190
191
    /**
192
     * Grab class from array.
193
     *
194
     * @param $data
195
     *
196
     * @return string|null
197
     */
198
    private function extractType(&$data): ?string
199
    {
200
        if (!$this->isObjectArray($data)) {
201
            return null;
202
        }
203
204
        $type = $data[self::TYPE_FIELD];
205
        unset($data[self::TYPE_FIELD]);
206
207
        return $type;
208
    }
209
210
    /**
211
     * To determine is passed data is a representation of some object.
212
     *
213
     * @param $data
214
     *
215
     * @return bool
216
     */
217
    private function isObjectArray($data): bool
218
    {
219
        return isset($data[self::TYPE_FIELD]);
220
    }
221
}
222