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

denormalizeObjectInOtherNormalizer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
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.
21
 *
22
 * @author Kévin Dunglas <[email protected]>
23
 */
24
final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
25
{
26
    // To not use the current normalizer
27
    const WONT_DENORMALIZE = 'dunglas_normalizer_wont_denormalize';
28
    const TYPE_FIELD = '#type';
29
30
    /**
31
     * @var NormalizerInterface|DenormalizerInterface
32
     */
33
    private $serializer;
34
    private $objectNormalizer;
35
36
    public function __construct(NormalizerInterface $objectNormalizer)
37
    {
38
        if (!$objectNormalizer instanceof DenormalizerInterface) {
39
            throw new \InvalidArgumentException(sprintf('The normalizer used must implement the "%s" interface.', DenormalizerInterface::class));
40
        }
41
42
        $this->objectNormalizer = $objectNormalizer;
43
    }
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function normalize($object, $format = null, array $context = [])
49
    {
50
        return \array_merge([self::TYPE_FIELD => ClassUtils::getClass($object)], $this->objectNormalizer->normalize($object, $format, $context));
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function supportsNormalization($data, $format = null)
57
    {
58
        return \is_object($data);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function denormalize($data, $class, $format = null, array $context = [])
65
    {
66
        if (!\is_iterable($data) || isset($context[self::WONT_DENORMALIZE]) || \is_object($data)) {
67
            return $data;
68
        }
69
70
        if (null !== $type = $this->extractType($data)) {
71
            return $this->denormalizeObject($data, $type, $format, $context);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 64 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...
72
        }
73
74
        foreach ($data as $key => $value) {
75
            $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 64 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...
76
        }
77
78
        return $data;
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function supportsDenormalization($data, $type, $format = null)
85
    {
86
        return true;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function setSerializer(SerializerInterface $serializer)
93
    {
94
        if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) {
95
            throw new \InvalidArgumentException(
96
                sprintf('The injected serializer must implement "%s" and "%s".', NormalizerInterface::class, DenormalizerInterface::class)
97
            );
98
        }
99
100
        $this->serializer = $serializer;
101
102
        if ($this->objectNormalizer instanceof SerializerAwareInterface) {
103
            $this->objectNormalizer->setSerializer($serializer);
104
        }
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function hasCacheableSupportsMethod(): bool
111
    {
112
        return true;
113
    }
114
115
    /**
116
     * Converts data to $class' object if possible.
117
     *
118
     * @param array  $data
119
     * @param string $class
120
     * @param null   $format
121
     * @param array  $context
122
     *
123
     * @return object|null
124
     */
125
    private function denormalizeObject(array $data, string $class, $format = null, array $context = [])
126
    {
127
        return $this->denormalizeObjectInOtherNormalizer($data, $class, $format, $context)
128
            ?? $this->denormalizeObjectInDefaultObjectNormalizer($data, $class, $format, $context);
129
    }
130
131
    /**
132
     * Tries to convert data to $class' object not using current normalizer.
133
     * This is useful if you have your own normalizers - they will have priority over this one.
134
     *
135
     * @param array  $data
136
     * @param string $class
137
     * @param null   $format
138
     * @param array  $context
139
     *
140
     * @return object|null
141
     */
142
    private function denormalizeObjectInOtherNormalizer(array $data, string $class, $format = null, array $context = [])
143
    {
144
        $context[self::WONT_DENORMALIZE] = true;
145
146
        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...
147
    }
148
149
    /**
150
     * Default denormalization $data to class using symfony's object normalizer.
151
     *
152
     * @param array  $data
153
     * @param string $class
154
     * @param null   $format
155
     * @param array  $context
156
     *
157
     * @return object
158
     */
159
    private function denormalizeObjectInDefaultObjectNormalizer(array $data, string $class, $format = null, array $context = [])
160
    {
161
        foreach ($data as $key => $value) {
162
            $data[$key] = $this->denormalizeValue($value, $class, $format, $context);
163
        }
164
165
        return $this->objectNormalizer->denormalize($data, $class, $format, $context);
166
    }
167
168
    /**
169
     * Convert raw value to normalized value - object or primitive type.
170
     *
171
     * @param mixed  $value
172
     * @param string $class
173
     * @param null   $format
174
     * @param array  $context
175
     *
176
     * @return object|null
177
     */
178
    private function denormalizeValue($value, string $class, $format = null, array $context = [])
179
    {
180
        if (\is_object($value)) {
181
            return $value;
182
        }
183
184
        return (null !== $type = $this->extractType($value))
185
            ? $this->denormalizeObject($value, $type, $format, $context)
186
            : $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...
187
    }
188
189
    /**
190
     * Grab class from array.
191
     *
192
     * @param $data
193
     *
194
     * @return string|null
195
     */
196
    private function extractType(&$data)
197
    {
198
        if (!isset($data[self::TYPE_FIELD])) {
199
            return null;
200
        }
201
202
        $type = $data[self::TYPE_FIELD];
203
        unset($data[self::TYPE_FIELD]);
204
205
        return $type;
206
    }
207
}
208