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

ObjectNormalizer::isObjectArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 (isset($context[self::WONT_DENORMALIZE]) || \is_object($data) || !\is_iterable($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
			if (!\is_object($value)) {
78
				$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...
79
			}
80
		}
81
82
		return $data;
83
	}
84
85
	/**
86
	 * {@inheritdoc}
87
	 */
88
	public function supportsDenormalization($data, $type, $format = null)
89
	{
90
		return true;
91
	}
92
93
	/**
94
	 * {@inheritdoc}
95
	 */
96
	public function setSerializer(SerializerInterface $serializer)
97
	{
98
		if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) {
99
			throw new \InvalidArgumentException(
100
				sprintf('The injected serializer must implement "%s" and "%s".', NormalizerInterface::class, DenormalizerInterface::class)
101
			);
102
		}
103
104
		$this->serializer = $serializer;
105
106
		if ($this->objectNormalizer instanceof SerializerAwareInterface) {
107
			$this->objectNormalizer->setSerializer($serializer);
108
		}
109
	}
110
111
	/**
112
	 * {@inheritdoc}
113
	 */
114
	public function hasCacheableSupportsMethod(): bool
115
	{
116
		return true;
117
	}
118
119
	/**
120
	 * Converts data to $class' object if possible.
121
	 *
122
	 * @param array  $data
123
	 * @param string $class
124
	 * @param null   $format
125
	 * @param array  $context
126
	 *
127
	 * @return object|null
128
	 */
129
	private function denormalizeObject(array $data, string $class, $format = null, array $context = [])
130
	{
131
		return $this->denormalizeObjectInOtherNormalizer($data, $class, $format, $context)
132
			?? $this->denormalizeObjectInDefaultObjectNormalizer($data, $class, $format, $context);
133
	}
134
135
	/**
136
	 * Tries to convert data to $class' object not using current normalizer.
137
	 * This is useful if you have your own normalizers - they will have priority over this one.
138
	 *
139
	 * @param array  $data
140
	 * @param string $class
141
	 * @param null   $format
142
	 * @param array  $context
143
	 *
144
	 * @return object|null
145
	 */
146
	private function denormalizeObjectInOtherNormalizer(array $data, string $class, $format = null, array $context = [])
147
	{
148
		$context[self::WONT_DENORMALIZE] = true;
149
150
		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...
151
	}
152
153
	/**
154
	 * Default denormalization $data to class using symfony's object normalizer.
155
	 *
156
	 * @param array  $data
157
	 * @param string $class
158
	 * @param null   $format
159
	 * @param array  $context
160
	 *
161
	 * @return object
162
	 */
163
	private function denormalizeObjectInDefaultObjectNormalizer(array $data, string $class, $format = null, array $context = [])
164
	{
165
		foreach ($data as $key => $value) {
166
			$data[$key] = $this->denormalizeValue($value, $class, $format, $context);
167
		}
168
169
		return $this->objectNormalizer->denormalize($data, $class, $format, $context);
170
	}
171
172
	/**
173
	 * Convert raw value to normalized value - object or primitive type.
174
	 *
175
	 * @param mixed  $value
176
	 * @param string $class
177
	 * @param null   $format
178
	 * @param array  $context
179
	 *
180
	 * @return object|null
181
	 */
182
	private function denormalizeValue($value, string $class, $format = null, array $context = [])
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): ?string
197
	{
198
		if (!$this->isObjectArray($data)) {
199
			return null;
200
		}
201
202
		$type = $data[self::TYPE_FIELD];
203
		unset($data[self::TYPE_FIELD]);
204
205
		return $type;
206
	}
207
208
	/**
209
	 * To determine is passed data is a representation of some object.
210
	 *
211
	 * @param $data
212
	 *
213
	 * @return bool
214
	 */
215
	private function isObjectArray($data): bool
216
	{
217
		return isset($data[self::TYPE_FIELD]);
218
	}
219
}
220