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