diff --git a/Mapping/Loader/AccessorCollisionResolverTrait.php b/Mapping/Loader/AccessorCollisionResolverTrait.php new file mode 100644 index 00000000000..8bd7d97cc4b --- /dev/null +++ b/Mapping/Loader/AccessorCollisionResolverTrait.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping\Loader; + +use Symfony\Component\Serializer\Attribute\Ignore; + +/** + * Provides methods to detect accessor name collisions during serialization. + * + * @internal + */ +trait AccessorCollisionResolverTrait +{ + private function getAttributeNameFromAccessor(\ReflectionClass $class, \ReflectionMethod $method, bool $andMutator): ?string + { + $methodName = $method->name; + + $i = match ($methodName[0]) { + 's' => $andMutator && str_starts_with($methodName, 'set') ? 3 : null, + 'g' => str_starts_with($methodName, 'get') ? 3 : null, + 'h' => str_starts_with($methodName, 'has') ? 3 : null, + 'c' => str_starts_with($methodName, 'can') ? 3 : null, + 'i' => str_starts_with($methodName, 'is') ? 2 : null, + default => null, + }; + + // ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel + if (null === $i || ctype_lower($methodName[$i] ?? 'a') || $method->isStatic()) { + return null; + } + + if ('s' === $methodName[0] ? !$method->getNumberOfParameters() : ($method->getNumberOfRequiredParameters() || \in_array((string) $method->getReturnType(), ['void', 'never'], true))) { + return null; + } + + $attributeName = substr($methodName, $i); + + if (!$class->hasProperty($attributeName)) { + $attributeName = lcfirst($attributeName); + } + + return $attributeName; + } + + private function hasPropertyForAccessor(\ReflectionClass $class, string $propName): bool + { + do { + if ($class->hasProperty($propName)) { + return true; + } + } while ($class = $class->getParentClass()); + + return false; + } + + private function hasAttributeNameCollision(\ReflectionClass $class, string $attributeName, string $methodName): bool + { + if ($this->hasPropertyForAccessor($class, $attributeName)) { + return true; + } + + if ($class->hasMethod($attributeName)) { + $candidate = $class->getMethod($attributeName); + if ($candidate->getName() !== $methodName && $this->isReadableAccessorMethod($candidate)) { + return true; + } + } + + $ucAttributeName = ucfirst($attributeName); + foreach (['get', 'is', 'has', 'can'] as $prefix) { + $candidateName = $prefix.$ucAttributeName; + if ($candidateName === $methodName || !$class->hasMethod($candidateName)) { + continue; + } + + if ($this->isReadableAccessorMethod($class->getMethod($candidateName))) { + return true; + } + } + + return false; + } + + private function isReadableAccessorMethod(\ReflectionMethod $method): bool + { + return $method->isPublic() + && !$method->isStatic() + && !$method->getAttributes(Ignore::class) + && !$method->getNumberOfRequiredParameters() + && !\in_array((string) $method->getReturnType(), ['void', 'never'], true); + } +} diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index d531609d18f..887544ff7ff 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -33,6 +33,8 @@ */ class AttributeLoader implements LoaderInterface { + use AccessorCollisionResolverTrait; + private const KNOWN_ATTRIBUTES = [ DiscriminatorMap::class, Groups::class, @@ -142,23 +144,13 @@ private function doLoadClassMetadata(\ReflectionClass $reflectionClass, ClassMet continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */ } - $accessorOrMutator = match ($name[0]) { - 's' => str_starts_with($name, 'set') && isset($name[$i = 3]), - 'g' => str_starts_with($name, 'get') && isset($name[$i = 3]), - 'h' => str_starts_with($name, 'has') && isset($name[$i = 3]), - 'c' => str_starts_with($name, 'can') && isset($name[$i = 3]), - 'i' => str_starts_with($name, 'is') && isset($name[$i = 2]), - default => false, - }; - if ($accessorOrMutator && !ctype_lower($name[$i])) { - if ($this->hasProperty($method->getDeclaringClass(), $name)) { - $attributeName = $name; - } else { - $attributeName = substr($name, $i); + $attributeName = $this->getAttributeNameFromAccessor($reflectionClass, $method, true); + $accessorOrMutator = null !== $attributeName; + $hasProperty = $this->hasPropertyForAccessor($method->getDeclaringClass(), $name); - if (!$reflectionClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); - } + if ($hasProperty || $accessorOrMutator) { + if (null === $attributeName || 's' !== $name[0] && $hasProperty && $this->hasAttributeNameCollision($reflectionClass, $attributeName, $name)) { + $attributeName = $name; } if (isset($attributesMetadata[$attributeName])) { @@ -171,7 +163,7 @@ private function doLoadClassMetadata(\ReflectionClass $reflectionClass, ClassMet foreach ($this->loadAttributes($method) as $attribute) { if ($attribute instanceof Groups) { - if (!$accessorOrMutator) { + if (!$accessorOrMutator && !$hasProperty) { throw new MappingException(\sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has", "can" or "set".', $className, $method->name)); } @@ -179,29 +171,27 @@ private function doLoadClassMetadata(\ReflectionClass $reflectionClass, ClassMet $attributeMetadata->addGroup($group); } } elseif ($attribute instanceof MaxDepth) { - if (!$accessorOrMutator) { + if (!$accessorOrMutator && !$hasProperty) { throw new MappingException(\sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has", "can" or "set".', $className, $method->name)); } $attributeMetadata->setMaxDepth($attribute->maxDepth); } elseif ($attribute instanceof SerializedName) { - if (!$accessorOrMutator) { + if (!$accessorOrMutator && !$hasProperty) { throw new MappingException(\sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has", "can" or "set".', $className, $method->name)); } $attributeMetadata->setSerializedName($attribute->serializedName); } elseif ($attribute instanceof SerializedPath) { - if (!$accessorOrMutator) { + if (!$accessorOrMutator && !$hasProperty) { throw new MappingException(\sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has", "can" or "set".', $className, $method->name)); } $attributeMetadata->setSerializedPath($attribute->serializedPath); } elseif ($attribute instanceof Ignore) { - if ($accessorOrMutator) { - $attributeMetadata->setIgnore(true); - } + $attributeMetadata->setIgnore(true); } elseif ($attribute instanceof Context) { - if (!$accessorOrMutator) { + if (!$accessorOrMutator && !$hasProperty) { throw new MappingException(\sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has", "can" or "set".', $className, $method->name)); } @@ -264,15 +254,4 @@ private function isKnownAttribute(string $attributeName): bool return false; } - - private function hasProperty(\ReflectionClass $class, string $propName): bool - { - do { - if ($class->hasProperty($propName)) { - return true; - } - } while ($class = $class->getParentClass()); - - return false; - } } diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 38a3fb471b4..1ea317de58c 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -347,6 +347,16 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a if (isset($nestedData[$notConverted]) && !isset($originalNestedData[$attribute])) { throw new LogicException(\sprintf('Duplicate values for key "%s" found. One value is set via the SerializedPath attribute: "%s", the other one is set via the SerializedName attribute: "%s".', $notConverted, implode('->', $nestedAttributes[$notConverted]->getElements()), $attribute)); } + + if ($attribute === $notConverted + && !($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES]) + && (false === $allowedAttributes || \in_array($attribute, $allowedAttributes, true)) + && $this->nameConverter->normalize($attribute, $resolvedClass, $format, $context) !== $attribute + ) { + // Input was in wrong format (e.g., camelCase when snake_case expected) + $extraAttributes[] = $notConverted; + continue; + } } $attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context); @@ -895,7 +905,7 @@ private function getNestedAttributes(string $class): array private function removeNestedValue(array $path, array $data): array { $element = array_shift($path); - if (!$path || !$data[$element] = $this->removeNestedValue($path, $data[$element])) { + if (!$path || !$data[$element] || !$data[$element] = $this->removeNestedValue($path, $data[$element])) { unset($data[$element]); } diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index b0ca3b4bd6d..ff8e0bf7d88 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -87,8 +87,9 @@ private function isGetMethod(\ReflectionMethod $method): bool return !$method->isStatic() && !($method->getAttributes(Ignore::class) || $method->getAttributes(LegacyIgnore::class)) && !$method->getNumberOfRequiredParameters() + && !\in_array((string) $method->getReturnType(), ['void', 'never'], true) && ((2 < ($methodLength = \strlen($method->name)) && str_starts_with($method->name, 'is') && !ctype_lower($method->name[2])) - || (3 < $methodLength && (str_starts_with($method->name, 'has') || str_starts_with($method->name, 'get')) && !ctype_lower($method->name[3])) + || (3 < $methodLength && (str_starts_with($method->name, 'has') || str_starts_with($method->name, 'get') || str_starts_with($method->name, 'can')) && !ctype_lower($method->name[3])) ); } diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index b1f8238d100..5889aa32086 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -18,10 +18,11 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfo; -use Symfony\Component\Serializer\Annotation\Ignore; +use Symfony\Component\Serializer\Attribute\Ignore; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\AccessorCollisionResolverTrait; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** @@ -31,6 +32,8 @@ */ final class ObjectNormalizer extends AbstractObjectNormalizer { + use AccessorCollisionResolverTrait; + private static $reflectionCache = []; private static $isReadableCache = []; private static $isWritableCache = []; @@ -75,35 +78,11 @@ protected function extractAttributes(object $object, ?string $format = null, arr $reflClass = new \ReflectionClass($class); foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) { - if ( - 0 !== $reflMethod->getNumberOfRequiredParameters() - || $reflMethod->isStatic() - || $reflMethod->isConstructor() - || $reflMethod->isDestructor() - ) { - continue; - } - $name = $reflMethod->name; - $attributeName = null; - - // ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel - if (match ($name[0]) { - 'g' => str_starts_with($name, 'get') && isset($name[$i = 3]), - 'h' => str_starts_with($name, 'has') && isset($name[$i = 3]), - 'c' => str_starts_with($name, 'can') && isset($name[$i = 3]), - 'i' => str_starts_with($name, 'is') && isset($name[$i = 2]), - default => false, - } && !ctype_lower($name[$i])) { - if ($this->hasProperty($reflMethod->getDeclaringClass(), $name)) { - $attributeName = $name; - } else { - $attributeName = substr($name, $i); - - if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); - } - } + $attributeName = $this->getAttributeNameFromAccessor($reflClass, $reflMethod, false); + + if ($this->hasPropertyForAccessor($reflMethod->getDeclaringClass(), $name) && (null === $attributeName || $this->hasAttributeNameCollision($reflClass, $attributeName, $name))) { + $attributeName = $name; } if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) { @@ -127,17 +106,6 @@ protected function extractAttributes(object $object, ?string $format = null, arr return array_keys($attributes); } - private function hasProperty(\ReflectionClass $class, string $propName): bool - { - do { - if ($class->hasProperty($propName)) { - return true; - } - } while ($class = $class->getParentClass()); - - return false; - } - protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { $mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object); @@ -183,11 +151,7 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string private function hasAttributeAccessorMethod(string $class, string $attribute): bool { - if (!isset(self::$reflectionCache[$class])) { - self::$reflectionCache[$class] = new \ReflectionClass($class); - } - - $reflection = self::$reflectionCache[$class]; + $reflection = self::$reflectionCache[$class] ??= new \ReflectionClass($class); if (!$reflection->hasMethod($attribute)) { return false; @@ -197,6 +161,7 @@ private function hasAttributeAccessorMethod(string $class, string $attribute): b return !$method->isStatic() && !$method->getAttributes(Ignore::class) - && !$method->getNumberOfRequiredParameters(); + && !$method->getNumberOfRequiredParameters() + && !\in_array((string) $method->getReturnType(), ['void', 'never'], true); } } diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index ffba4f49776..3d86af9a8fb 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -15,7 +15,6 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Serializer\Command\DebugCommand; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; -use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Tests\Dummy\DummyClassOne; use Symfony\Component\Serializer\Tests\Dummy\DummyClassWithDiscriminatorMap; @@ -117,9 +116,7 @@ public function testOutputWithDiscriminatorMapClass() public function testOutputWithInvalidClassArgument() { - $serializer = $this->createMock(ClassMetadataFactoryInterface::class); - - $command = new DebugCommand($serializer); + $command = new DebugCommand(new ClassMetadataFactory(new AttributeLoader())); $tester = new CommandTester($command); $tester->execute(['class' => 'App\\NotFoundResource'], ['decorated' => false]); diff --git a/Tests/Debug/TraceableEncoderTest.php b/Tests/Debug/TraceableEncoderTest.php index f288158f75e..82867f3e950 100644 --- a/Tests/Debug/TraceableEncoderTest.php +++ b/Tests/Debug/TraceableEncoderTest.php @@ -44,8 +44,8 @@ public function testCollectEncodingData() { $serializerName = uniqid('name', true); - $encoder = $this->createMock(EncoderInterface::class); - $decoder = $this->createMock(DecoderInterface::class); + $encoder = $this->createStub(EncoderInterface::class); + $decoder = $this->createStub(DecoderInterface::class); $dataCollector = $this->createMock(SerializerDataCollector::class); $dataCollector @@ -63,8 +63,8 @@ public function testCollectEncodingData() public function testNotCollectEncodingDataIfNoDebugTraceId() { - $encoder = $this->createMock(EncoderInterface::class); - $decoder = $this->createMock(DecoderInterface::class); + $encoder = $this->createStub(EncoderInterface::class); + $decoder = $this->createStub(DecoderInterface::class); $dataCollector = $this->createMock(SerializerDataCollector::class); $dataCollector->expects($this->never())->method('collectEncoding'); @@ -78,22 +78,22 @@ public function testCannotEncodeIfNotEncoder() { $this->expectException(\BadMethodCallException::class); - (new TraceableEncoder($this->createMock(DecoderInterface::class), new SerializerDataCollector(), 'default'))->encode('data', 'format'); + (new TraceableEncoder($this->createStub(DecoderInterface::class), new SerializerDataCollector(), 'default'))->encode('data', 'format'); } public function testCannotDecodeIfNotDecoder() { $this->expectException(\BadMethodCallException::class); - (new TraceableEncoder($this->createMock(EncoderInterface::class), new SerializerDataCollector(), 'default'))->decode('data', 'format'); + (new TraceableEncoder($this->createStub(EncoderInterface::class), new SerializerDataCollector(), 'default'))->decode('data', 'format'); } public function testSupports() { - $encoder = $this->createMock(EncoderInterface::class); + $encoder = $this->createStub(EncoderInterface::class); $encoder->method('supportsEncoding')->willReturn(true); - $decoder = $this->createMock(DecoderInterface::class); + $decoder = $this->createStub(DecoderInterface::class); $decoder->method('supportsDecoding')->willReturn(true); $traceableEncoder = new TraceableEncoder($encoder, new SerializerDataCollector(), 'default'); diff --git a/Tests/Debug/TraceableNormalizerTest.php b/Tests/Debug/TraceableNormalizerTest.php index e7a023d9638..ed069fdeba2 100644 --- a/Tests/Debug/TraceableNormalizerTest.php +++ b/Tests/Debug/TraceableNormalizerTest.php @@ -46,9 +46,9 @@ public function testCollectNormalizationData() { $serializerName = uniqid('name', true); - $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer = $this->createStub(NormalizerInterface::class); $normalizer->method('getSupportedTypes')->willReturn(['*' => false]); - $denormalizer = $this->createMock(DenormalizerInterface::class); + $denormalizer = $this->createStub(DenormalizerInterface::class); $denormalizer->method('getSupportedTypes')->willReturn(['*' => false]); $dataCollector = $this->createMock(SerializerDataCollector::class); @@ -67,9 +67,9 @@ public function testCollectNormalizationData() public function testNotCollectNormalizationDataIfNoDebugTraceId() { - $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer = $this->createStub(NormalizerInterface::class); $normalizer->method('getSupportedTypes')->willReturn(['*' => false]); - $denormalizer = $this->createMock(DenormalizerInterface::class); + $denormalizer = $this->createStub(DenormalizerInterface::class); $denormalizer->method('getSupportedTypes')->willReturn(['*' => false]); $dataCollector = $this->createMock(SerializerDataCollector::class); @@ -84,23 +84,23 @@ public function testCannotNormalizeIfNotNormalizer() { $this->expectException(\BadMethodCallException::class); - (new TraceableNormalizer($this->createMock(DenormalizerInterface::class), new SerializerDataCollector(), 'default'))->normalize('data'); + (new TraceableNormalizer($this->createStub(DenormalizerInterface::class), new SerializerDataCollector(), 'default'))->normalize('data'); } public function testCannotDenormalizeIfNotDenormalizer() { $this->expectException(\BadMethodCallException::class); - (new TraceableNormalizer($this->createMock(NormalizerInterface::class), new SerializerDataCollector(), 'default'))->denormalize('data', 'type'); + (new TraceableNormalizer($this->createStub(NormalizerInterface::class), new SerializerDataCollector(), 'default'))->denormalize('data', 'type'); } public function testSupports() { - $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer = $this->createStub(NormalizerInterface::class); $normalizer->method('getSupportedTypes')->willReturn(['*' => false]); $normalizer->method('supportsNormalization')->willReturn(true); - $denormalizer = $this->createMock(DenormalizerInterface::class); + $denormalizer = $this->createStub(DenormalizerInterface::class); $denormalizer->method('getSupportedTypes')->willReturn(['*' => false]); $denormalizer->method('supportsDenormalization')->willReturn(true); diff --git a/Tests/Debug/TraceableSerializerTest.php b/Tests/Debug/TraceableSerializerTest.php index 5a55e3ca7a6..196c02e0d3f 100644 --- a/Tests/Debug/TraceableSerializerTest.php +++ b/Tests/Debug/TraceableSerializerTest.php @@ -108,7 +108,7 @@ public function testCollectData() public function testAddDebugTraceIdInContext() { - $serializer = $this->createMock(Serializer::class); + $serializer = $this->createStub(Serializer::class); foreach (['serialize', 'deserialize', 'normalize', 'denormalize', 'encode', 'decode'] as $method) { $serializer->method($method)->willReturnCallback(function (): string { diff --git a/Tests/Encoder/ChainDecoderTest.php b/Tests/Encoder/ChainDecoderTest.php index c3bbad2ab9e..f77f7285ecc 100644 --- a/Tests/Encoder/ChainDecoderTest.php +++ b/Tests/Encoder/ChainDecoderTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Tests\Encoder; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\ChainDecoder; use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface; @@ -24,72 +23,90 @@ class ChainDecoderTest extends TestCase private const FORMAT_2 = 'format2'; private const FORMAT_3 = 'format3'; - private ChainDecoder $chainDecoder; - private MockObject&ContextAwareDecoderInterface $decoder1; - private MockObject&DecoderInterface $decoder2; - - protected function setUp(): void - { - $this->decoder1 = $this->createMock(ContextAwareDecoderInterface::class); - $this->decoder1 - ->method('supportsDecoding') - ->willReturnMap([ - [self::FORMAT_1, [], true], - [self::FORMAT_2, [], false], - [self::FORMAT_3, [], false], - [self::FORMAT_3, ['foo' => 'bar'], true], - [self::FORMAT_3, ['foo' => 'bar2'], false], - ]); - - $this->decoder2 = $this->createMock(DecoderInterface::class); - $this->decoder2 - ->method('supportsDecoding') - ->willReturnMap([ - [self::FORMAT_1, [], false], - [self::FORMAT_2, [], true], - [self::FORMAT_3, [], false], - [self::FORMAT_3, ['foo' => 'bar'], false], - [self::FORMAT_3, ['foo' => 'bar2'], true], - ]); - - $this->chainDecoder = new ChainDecoder([$this->decoder1, $this->decoder2]); - } - public function testSupportsDecoding() { - $this->decoder1 + $decoder1 = $this->createDecoder1(); + $decoder1 ->method('decode') ->willReturn('result1'); - $this->decoder2 + $decoder2 = $this->createDecoder2(); + $decoder2 ->method('decode') ->willReturn('result2'); + $chainDecoder = new ChainDecoder([$decoder1, $decoder2]); - $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_1)); - $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_1, [])); + $this->assertTrue($chainDecoder->supportsDecoding(self::FORMAT_1)); + $this->assertEquals('result1', $chainDecoder->decode('', self::FORMAT_1, [])); - $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_2)); - $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_2, [])); + $this->assertTrue($chainDecoder->supportsDecoding(self::FORMAT_2)); + $this->assertEquals('result2', $chainDecoder->decode('', self::FORMAT_2, [])); - $this->assertFalse($this->chainDecoder->supportsDecoding(self::FORMAT_3)); + $this->assertFalse($chainDecoder->supportsDecoding(self::FORMAT_3)); - $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar'])); - $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar'])); + $this->assertTrue($chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar'])); + $this->assertEquals('result1', $chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar'])); - $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar2'])); - $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertTrue($chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertEquals('result2', $chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar2'])); } public function testDecode() { - $this->decoder1->expects($this->never())->method('decode'); - $this->decoder2->expects($this->once())->method('decode'); + $decoder1 = $this->createDecoder1(true); + $decoder1->expects($this->never())->method('decode'); + $decoder2 = $this->createDecoder2(true); + $decoder2->expects($this->once())->method('decode'); + $chainDecoder = new ChainDecoder([$decoder1, $decoder2]); - $this->chainDecoder->decode('string_to_decode', self::FORMAT_2); + $chainDecoder->decode('string_to_decode', self::FORMAT_2); } public function testDecodeUnsupportedFormat() { + $chainDecoder = new ChainDecoder([$this->createDecoder1(), $this->createDecoder2()]); $this->expectException(RuntimeException::class); - $this->chainDecoder->decode('string_to_decode', self::FORMAT_3); + $chainDecoder->decode('string_to_decode', self::FORMAT_3); + } + + private function createDecoder1(bool $mock = false): DecoderInterface + { + if ($mock) { + $decoder = $this->createMock(ContextAwareDecoderInterface::class); + } else { + $decoder = $this->createStub(ContextAwareDecoderInterface::class); + } + + $decoder + ->method('supportsDecoding') + ->willReturnMap([ + [self::FORMAT_1, [], true], + [self::FORMAT_2, [], false], + [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], true], + [self::FORMAT_3, ['foo' => 'bar2'], false], + ]); + + return $decoder; + } + + private function createDecoder2(bool $mock = false): DecoderInterface + { + if ($mock) { + $decoder = $this->createMock(DecoderInterface::class); + } else { + $decoder = $this->createStub(DecoderInterface::class); + } + + $decoder + ->method('supportsDecoding') + ->willReturnMap([ + [self::FORMAT_1, [], false], + [self::FORMAT_2, [], true], + [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], false], + [self::FORMAT_3, ['foo' => 'bar2'], true], + ]); + + return $decoder; } } diff --git a/Tests/Encoder/ChainEncoderTest.php b/Tests/Encoder/ChainEncoderTest.php index 375d08054ab..cb7048f780c 100644 --- a/Tests/Encoder/ChainEncoderTest.php +++ b/Tests/Encoder/ChainEncoderTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Tests\Encoder; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Encoder\ChainEncoder; @@ -26,79 +25,59 @@ class ChainEncoderTest extends TestCase private const FORMAT_2 = 'format2'; private const FORMAT_3 = 'format3'; - private ChainEncoder $chainEncoder; - private MockObject&ContextAwareEncoderInterface $encoder1; - private MockObject&EncoderInterface $encoder2; - - protected function setUp(): void - { - $this->encoder1 = $this->createMock(ContextAwareEncoderInterface::class); - $this->encoder1 - ->method('supportsEncoding') - ->willReturnMap([ - [self::FORMAT_1, [], true], - [self::FORMAT_2, [], false], - [self::FORMAT_3, [], false], - [self::FORMAT_3, ['foo' => 'bar'], true], - [self::FORMAT_3, ['foo' => 'bar2'], false], - ]); - - $this->encoder2 = $this->createMock(EncoderInterface::class); - $this->encoder2 - ->method('supportsEncoding') - ->willReturnMap([ - [self::FORMAT_1, [], false], - [self::FORMAT_2, [], true], - [self::FORMAT_3, [], false], - [self::FORMAT_3, ['foo' => 'bar'], false], - [self::FORMAT_3, ['foo' => 'bar2'], true], - ]); - - $this->chainEncoder = new ChainEncoder([$this->encoder1, $this->encoder2]); - } - public function testSupportsEncoding() { - $this->encoder1 + $encoder1 = $this->createEncoder1(); + $encoder1 ->method('encode') ->willReturn('result1'); - $this->encoder2 + $encoder2 = $this->createEncoder2(); + $encoder2 ->method('encode') ->willReturn('result2'); - $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_1)); - $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_1, [])); + $chainEncoder = new ChainEncoder([$encoder1, $encoder2]); + + $this->assertTrue($chainEncoder->supportsEncoding(self::FORMAT_1)); + $this->assertEquals('result1', $chainEncoder->encode('', self::FORMAT_1, [])); - $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_2)); - $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_2, [])); + $this->assertTrue($chainEncoder->supportsEncoding(self::FORMAT_2)); + $this->assertEquals('result2', $chainEncoder->encode('', self::FORMAT_2, [])); - $this->assertFalse($this->chainEncoder->supportsEncoding(self::FORMAT_3)); + $this->assertFalse($chainEncoder->supportsEncoding(self::FORMAT_3)); - $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar'])); - $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar'])); + $this->assertTrue($chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar'])); + $this->assertEquals('result1', $chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar'])); - $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar2'])); - $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertTrue($chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertEquals('result2', $chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar2'])); } public function testEncode() { - $this->encoder1->expects($this->never())->method('encode'); - $this->encoder2->expects($this->once())->method('encode')->willReturn('foo:123'); + $encoder1 = $this->createEncoder1(true); + $encoder1->expects($this->never())->method('encode'); + $encoder2 = $this->createEncoder2(true); + $encoder2->expects($this->once())->method('encode')->willReturn('foo:123'); - $this->assertSame('foo:123', $this->chainEncoder->encode(['foo' => 123], self::FORMAT_2)); + $chainEncoder = new ChainEncoder([$encoder1, $encoder2]); + + $this->assertSame('foo:123', $chainEncoder->encode(['foo' => 123], self::FORMAT_2)); } public function testEncodeUnsupportedFormat() { + $chainEncoder = new ChainEncoder([$this->createEncoder1(), $this->createEncoder2()]); $this->expectException(RuntimeException::class); - $this->chainEncoder->encode(['foo' => 123], self::FORMAT_3); + $chainEncoder->encode(['foo' => 123], self::FORMAT_3); } public function testNeedsNormalizationBasic() { - $this->assertTrue($this->chainEncoder->needsNormalization(self::FORMAT_1)); - $this->assertTrue($this->chainEncoder->needsNormalization(self::FORMAT_2)); + $chainEncoder = new ChainEncoder([$this->createEncoder1(), $this->createEncoder2()]); + + $this->assertTrue($chainEncoder->needsNormalization(self::FORMAT_1)); + $this->assertTrue($chainEncoder->needsNormalization(self::FORMAT_2)); } public function testNeedsNormalizationNormalizationAware() @@ -111,18 +90,60 @@ public function testNeedsNormalizationNormalizationAware() public function testNeedsNormalizationTraceableEncoder() { - $traceableEncoder = $this->createMock(TraceableEncoder::class); + $traceableEncoder = $this->createStub(TraceableEncoder::class); $traceableEncoder->method('needsNormalization')->willReturn(true); $traceableEncoder->method('supportsEncoding')->willReturn(true); $this->assertTrue((new ChainEncoder([$traceableEncoder]))->needsNormalization('format')); - $traceableEncoder = $this->createMock(TraceableEncoder::class); + $traceableEncoder = $this->createStub(TraceableEncoder::class); $traceableEncoder->method('needsNormalization')->willReturn(false); $traceableEncoder->method('supportsEncoding')->willReturn(true); $this->assertFalse((new ChainEncoder([$traceableEncoder]))->needsNormalization('format')); } + + private function createEncoder1(bool $mock = false): EncoderInterface + { + if ($mock) { + $encoder = $this->createMock(ContextAwareEncoderInterface::class); + } else { + $encoder = $this->createStub(ContextAwareEncoderInterface::class); + } + + $encoder + ->method('supportsEncoding') + ->willReturnMap([ + [self::FORMAT_1, [], true], + [self::FORMAT_2, [], false], + [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], true], + [self::FORMAT_3, ['foo' => 'bar2'], false], + ]); + + return $encoder; + } + + private function createEncoder2(bool $mock = false): EncoderInterface + { + if ($mock) { + $encoder = $this->createMock(EncoderInterface::class); + } else { + $encoder = $this->createStub(EncoderInterface::class); + } + + $encoder + ->method('supportsEncoding') + ->willReturnMap([ + [self::FORMAT_1, [], false], + [self::FORMAT_2, [], true], + [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], false], + [self::FORMAT_3, ['foo' => 'bar2'], true], + ]); + + return $encoder; + } } class NormalizationAwareEncoder implements EncoderInterface, NormalizationAwareInterface diff --git a/Tests/Fixtures/Attributes/AccessorishGetters.php b/Tests/Fixtures/Attributes/AccessorishGetters.php index f434e84f33d..80cdb8abea0 100644 --- a/Tests/Fixtures/Attributes/AccessorishGetters.php +++ b/Tests/Fixtures/Attributes/AccessorishGetters.php @@ -33,7 +33,7 @@ public function hasField3() { } - public function setField4() + public function setField4($value) { } } diff --git a/Tests/Fixtures/VoidNeverReturnTypeDummy.php b/Tests/Fixtures/VoidNeverReturnTypeDummy.php new file mode 100644 index 00000000000..eb083d449e8 --- /dev/null +++ b/Tests/Fixtures/VoidNeverReturnTypeDummy.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +class VoidNeverReturnTypeDummy +{ + public string $normalProperty = 'value'; + + public function getNormalProperty(): string + { + return $this->normalProperty; + } + + public function getVoidProperty(): void + { + // This looks like a getter but returns void, should be ignored + } + + public function getNeverProperty(): never + { + // This looks like a getter but returns never, should be ignored + throw new \Exception('Never returns'); + } + + public function setValue(): void + { + // This looks like a setter but has no parameters, should be ignored as accessor + } + + public function setNeverValue(): never + { + // This looks like a setter but has no parameters and returns never, should be ignored as accessor + throw new \Exception('Never returns'); + } +} + diff --git a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php index e18dd707f91..6da0eaab0cd 100644 --- a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php @@ -16,7 +16,9 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; /** @@ -58,8 +60,7 @@ public function testHasMetadataFor() public function testInvalidClassThrowsException() { - $decorated = $this->createMock(ClassMetadataFactoryInterface::class); - $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + $factory = new CacheClassMetadataFactory(new ClassMetadataFactory(new AttributeLoader()), new ArrayAdapter()); $this->expectException(InvalidArgumentException::class); diff --git a/Tests/NameConverter/MetadataAwareNameConverterTest.php b/Tests/NameConverter/MetadataAwareNameConverterTest.php index f39f3e3fade..ffaedab44ef 100644 --- a/Tests/NameConverter/MetadataAwareNameConverterTest.php +++ b/Tests/NameConverter/MetadataAwareNameConverterTest.php @@ -17,7 +17,6 @@ use Symfony\Component\Serializer\Attribute\SerializedPath; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; -use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -31,8 +30,7 @@ final class MetadataAwareNameConverterTest extends TestCase { public function testInterface() { - $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); - $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $nameConverter = new MetadataAwareNameConverter(new ClassMetadataFactory(new AttributeLoader())); $this->assertInstanceOf(NameConverterInterface::class, $nameConverter); } @@ -51,7 +49,7 @@ public function testNormalizeWithFallback(string|int $propertyName, string|int $ { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); - $fallback = $this->createMock(NameConverterInterface::class); + $fallback = $this->createStub(NameConverterInterface::class); $fallback ->method('normalize') ->willReturnCallback(static fn ($propertyName) => strtoupper($propertyName)) @@ -77,7 +75,7 @@ public function testDenormalizeWithFallback(string|int $expected, string|int $pr { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); - $fallback = $this->createMock(NameConverterInterface::class); + $fallback = $this->createStub(NameConverterInterface::class); $fallback ->method('denormalize') ->willReturnCallback(static fn ($propertyName) => strtolower($propertyName)) diff --git a/Tests/Normalizer/AbstractNormalizerTest.php b/Tests/Normalizer/AbstractNormalizerTest.php index 2cccf95ed06..8a6d0bf54d4 100644 --- a/Tests/Normalizer/AbstractNormalizerTest.php +++ b/Tests/Normalizer/AbstractNormalizerTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -20,9 +19,7 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadata; -use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; -use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -47,12 +44,11 @@ class AbstractNormalizerTest extends TestCase { private AbstractNormalizerDummy $normalizer; - private MockObject&ClassMetadataFactoryInterface $classMetadata; + private ClassMetadataFactoryInterface $classMetadata; protected function setUp(): void { - $loader = $this->getMockBuilder(LoaderChain::class)->setConstructorArgs([[]])->getMock(); - $this->classMetadata = $this->getMockBuilder(ClassMetadataFactory::class)->setConstructorArgs([$loader])->getMock(); + $this->classMetadata = $this->createStub(ClassMetadataFactoryInterface::class); $this->normalizer = new AbstractNormalizerDummy($this->classMetadata); } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 8ed033d359a..65b404fa039 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -438,7 +439,7 @@ public function testDenormalizeCollectionDecodedFromXmlWithTwoChildren() private function getDenormalizerForDummyCollection() { - $extractor = $this->createMock(PhpDocExtractor::class); + $extractor = $this->createStub(PhpDocExtractor::class); $extractor->method('getType')->willReturn(Type::list(Type::object(DummyChild::class)), null); $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); @@ -489,7 +490,7 @@ public function testDenormalizeNotSerializableObjectToPopulate() private function getDenormalizerForStringCollection() { - $extractor = $this->createMock(PhpDocExtractor::class); + $extractor = $this->createStub(PhpDocExtractor::class); $extractor->method('getType')->willReturn(Type::list(Type::string()), null); $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); @@ -770,7 +771,7 @@ public function testDenormalizeBasicTypePropertiesFromXml() private function getDenormalizerForObjectWithBasicProperties() { - $extractor = $this->createMock(PhpDocExtractor::class); + $extractor = $this->createStub(PhpDocExtractor::class); $extractor->method('getType') ->willReturn( Type::bool(), @@ -981,6 +982,42 @@ public function testDenormalizeMissingAndNullNestedValues() $this->assertFalse((new \ReflectionProperty($obj, 'bar'))->isInitialized($obj)); } + public function testDenormalizeNullCoalescingValues() + { + if (!method_exists(PropertyPath::class, 'isNullSafe')) { + $this->markTestSkipped('null coalescing property path is not supported before symfony/property-access 6.2'); + } + + $normalizer = new AbstractObjectNormalizerWithMetadata(); + + $data = [ + 'data' => [ + 'foo' => 'test', + ], + 'empty_data' => null, + ]; + + $obj = new class { + #[SerializedPath('[data][foo?]')] + public ?string $foo; + + #[SerializedPath('[data][bar?]')] + public ?string $bar; + + #[SerializedPath('[empty_data?][nothing]')] + public ?string $nothing; + + #[SerializedPath('[not_set?][nothing]')] + public ?string $notSet; + }; + + $test = $normalizer->denormalize($data, $obj::class); + $this->assertSame('test', $test->foo); + $this->assertFalse((new \ReflectionProperty($obj, 'bar'))->isInitialized($obj)); + $this->assertNull($test->nothing); + $this->assertFalse((new \ReflectionProperty($obj, 'notSet'))->isInitialized($obj)); + } + public function testNormalizeBasedOnAllowedAttributes() { $normalizer = new class extends AbstractObjectNormalizer { diff --git a/Tests/Normalizer/ArrayDenormalizerTest.php b/Tests/Normalizer/ArrayDenormalizerTest.php index b60f57bac8c..2dd51bf852d 100644 --- a/Tests/Normalizer/ArrayDenormalizerTest.php +++ b/Tests/Normalizer/ArrayDenormalizerTest.php @@ -11,23 +11,12 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; class ArrayDenormalizerTest extends TestCase { - private ArrayDenormalizer $denormalizer; - private MockObject&DenormalizerInterface $serializer; - - protected function setUp(): void - { - $this->serializer = $this->createMock(DenormalizerInterface::class); - $this->denormalizer = new ArrayDenormalizer(); - $this->denormalizer->setDenormalizer($this->serializer); - } - public function testDenormalize() { $series = [ @@ -35,7 +24,8 @@ public function testDenormalize() [[['foo' => 'three', 'bar' => 'four']], new ArrayDummy('three', 'four')], ]; - $this->serializer->expects($this->exactly(2)) + $nestedDenormalizer = $this->createMock(DenormalizerInterface::class); + $nestedDenormalizer->expects($this->exactly(2)) ->method('denormalize') ->willReturnCallback(function ($data) use (&$series) { [$expectedArgs, $return] = array_shift($series); @@ -45,7 +35,9 @@ public function testDenormalize() }) ; - $result = $this->denormalizer->denormalize( + $denormalizer = new ArrayDenormalizer(); + $denormalizer->setDenormalizer($nestedDenormalizer); + $result = $denormalizer->denormalize( [ ['foo' => 'one', 'bar' => 'two'], ['foo' => 'three', 'bar' => 'four'], @@ -64,13 +56,16 @@ public function testDenormalize() public function testSupportsValidArray() { - $this->serializer->expects($this->once()) + $nestedDenormalizer = $this->createMock(DenormalizerInterface::class); + $nestedDenormalizer->expects($this->once()) ->method('supportsDenormalization') ->with($this->anything(), ArrayDummy::class, 'json', ['con' => 'text']) ->willReturn(true); + $denormalizer = new ArrayDenormalizer(); + $denormalizer->setDenormalizer($nestedDenormalizer); $this->assertTrue( - $this->denormalizer->supportsDenormalization( + $denormalizer->supportsDenormalization( [ ['foo' => 'one', 'bar' => 'two'], ['foo' => 'three', 'bar' => 'four'], @@ -84,12 +79,15 @@ public function testSupportsValidArray() public function testSupportsInvalidArray() { - $this->serializer->expects($this->any()) + $nestedDenormalizer = $this->createStub(DenormalizerInterface::class); + $nestedDenormalizer ->method('supportsDenormalization') ->willReturn(false); + $denormalizer = new ArrayDenormalizer(); + $denormalizer->setDenormalizer($nestedDenormalizer); $this->assertFalse( - $this->denormalizer->supportsDenormalization( + $denormalizer->supportsDenormalization( [ ['foo' => 'one', 'bar' => 'two'], ['foo' => 'three', 'bar' => 'four'], @@ -101,8 +99,11 @@ public function testSupportsInvalidArray() public function testSupportsNoArray() { + $denormalizer = new ArrayDenormalizer(); + $denormalizer->setDenormalizer($this->createStub(DenormalizerInterface::class)); + $this->assertFalse( - $this->denormalizer->supportsDenormalization( + $denormalizer->supportsDenormalization( ['foo' => 'one', 'bar' => 'two'], ArrayDummy::class ) diff --git a/Tests/Normalizer/FormErrorNormalizerTest.php b/Tests/Normalizer/FormErrorNormalizerTest.php index 1b50584b6df..0592be1815c 100644 --- a/Tests/Normalizer/FormErrorNormalizerTest.php +++ b/Tests/Normalizer/FormErrorNormalizerTest.php @@ -26,7 +26,7 @@ protected function setUp(): void { $this->normalizer = new FormErrorNormalizer(); - $this->form = $this->createMock(FormInterface::class); + $this->form = $this->createStub(FormInterface::class); $this->form->method('isSubmitted')->willReturn(true); $this->form->method('all')->willReturn([]); @@ -45,7 +45,7 @@ public function testSupportsNormalizationWithWrongClass() public function testSupportsNormalizationWithNotSubmittedForm() { - $form = $this->createMock(FormInterface::class); + $form = $this->createStub(FormInterface::class); $this->assertFalse($this->normalizer->supportsNormalization($form)); } @@ -117,7 +117,7 @@ public function testNormalizeWithChildren() ], ]; - $form = clone $form1 = clone $form2 = clone $form3 = $this->createMock(FormInterface::class); + $form = clone $form1 = clone $form2 = clone $form3 = $this->createStub(FormInterface::class); $form1->method('getErrors') ->willReturn(new FormErrorIterator($form1, [ @@ -142,7 +142,7 @@ public function testNormalizeWithChildren() $form2->method('all')->willReturn([$form3]); - $form = $this->createMock(FormInterface::class); + $form = $this->createStub(FormInterface::class); $form->method('isSubmitted')->willReturn(true); $form->method('all')->willReturn([$form1, $form2]); $form->method('getErrors') diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 139cfeb65fe..04947c499a3 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -38,6 +38,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\ScalarNormalizer; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\VoidNeverReturnTypeDummy; use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; @@ -401,7 +402,7 @@ protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer public function testUnableToNormalizeObjectAttribute() { - $this->normalizer->setSerializer($this->createMock(SerializerInterface::class)); + $this->normalizer->setSerializer($this->createStub(SerializerInterface::class)); $obj = new GetSetDummy(); $object = new \stdClass(); @@ -625,6 +626,16 @@ public function testDiscriminatorWithAllowExtraAttributesFalse() $this->assertInstanceOf(GetSetMethodDiscriminatedDummyOne::class, $obj); } + + public function testSkipVoidNeverReturnTypeAccessors() + { + $obj = new VoidNeverReturnTypeDummy(); + $normalized = $this->normalizer->normalize($obj); + $this->assertArrayHasKey('normalProperty', $normalized); + $this->assertArrayNotHasKey('voidProperty', $normalized); + $this->assertArrayNotHasKey('neverProperty', $normalized); + $this->assertEquals('value', $normalized['normalProperty']); + } } class GetSetDummy diff --git a/Tests/Normalizer/JsonSerializableNormalizerTest.php b/Tests/Normalizer/JsonSerializableNormalizerTest.php index f8f8546d7cb..39dce311e20 100644 --- a/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; @@ -30,30 +29,19 @@ class JsonSerializableNormalizerTest extends TestCase { use CircularReferenceTestTrait; - private JsonSerializableNormalizer $normalizer; - private MockObject&JsonSerializerNormalizer $serializer; - - protected function setUp(): void - { - $this->createNormalizer(); - } - - private function createNormalizer(array $defaultContext = []) - { - $this->serializer = $this->createMock(JsonSerializerNormalizer::class); - $this->normalizer = new JsonSerializableNormalizer(null, null, $defaultContext); - $this->normalizer->setSerializer($this->serializer); - } - public function testSupportNormalization() { - $this->assertTrue($this->normalizer->supportsNormalization(new JsonSerializableDummy())); - $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + $normalizer = new JsonSerializableNormalizer(); + $normalizer->setSerializer($this->createStub(JsonSerializerNormalizer::class)); + + $this->assertTrue($normalizer->supportsNormalization(new JsonSerializableDummy())); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass())); } public function testNormalize() { - $this->serializer + $serializer = $this->createMock(JsonSerializerNormalizer::class); + $serializer ->expects($this->once()) ->method('normalize') ->willReturnCallback(function ($data) { @@ -62,27 +50,31 @@ public function testNormalize() return 'string_object'; }) ; + $normalizer = new JsonSerializableNormalizer(); + $normalizer->setSerializer($serializer); - $this->assertEquals('string_object', $this->normalizer->normalize(new JsonSerializableDummy())); + $this->assertEquals('string_object', $normalizer->normalize(new JsonSerializableDummy())); } public function testCircularNormalize() { - $this->createNormalizer([JsonSerializableNormalizer::CIRCULAR_REFERENCE_LIMIT => 1]); + $normalizer = new JsonSerializableNormalizer(null, null, [JsonSerializableNormalizer::CIRCULAR_REFERENCE_LIMIT => 1]); $this->expectException(CircularReferenceException::class); - $this->serializer + $serializer = $this->createMock(JsonSerializerNormalizer::class); + $serializer ->expects($this->once()) ->method('normalize') - ->willReturnCallback(function ($data, $format, $context) { - $this->normalizer->normalize($data['qux'], $format, $context); + ->willReturnCallback(function ($data, $format, $context) use ($normalizer) { + $normalizer->normalize($data['qux'], $format, $context); return 'string_object'; }) ; + $normalizer->setSerializer($serializer); - $this->assertEquals('string_object', $this->normalizer->normalize(new JsonSerializableDummy())); + $this->assertEquals('string_object', $normalizer->normalize(new JsonSerializableDummy())); } protected function getNormalizerForCircularReference(array $defaultContext): JsonSerializableNormalizer @@ -100,9 +92,12 @@ protected function getSelfReferencingModel() public function testInvalidDataThrowException() { + $normalizer = new JsonSerializableNormalizer(); + $normalizer->setSerializer($this->createStub(JsonSerializerNormalizer::class)); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The object must implement "JsonSerializable".'); - $this->normalizer->normalize(new \stdClass()); + $normalizer->normalize(new \stdClass()); } } diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index df451b6923e..205e38d3eac 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -19,7 +19,9 @@ use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -50,6 +52,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Tests\Fixtures\StdClassNormalizer; +use Symfony\Component\Serializer\Tests\Fixtures\VoidNeverReturnTypeDummy; use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait; @@ -295,7 +298,7 @@ public function testConstructorWithObjectDenormalize() public function testConstructorWithObjectDenormalizeUsingPropertyInfoExtractor() { - $serializer = $this->createMock(ObjectSerializerNormalizer::class); + $serializer = $this->createStub(ObjectSerializerNormalizer::class); $normalizer = new ObjectNormalizer(null, null, null, null, null, null, [], new PropertyInfoExtractor()); $normalizer->setSerializer($serializer); @@ -688,7 +691,7 @@ protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer public function testUnableToNormalizeObjectAttribute() { - $serializer = $this->createMock(SerializerInterface::class); + $serializer = $this->createStub(SerializerInterface::class); $this->normalizer->setSerializer($serializer); $obj = new ObjectDummy(); @@ -1138,6 +1141,39 @@ public function testNormalizeChildWithPropertySameAsParentMethod() ], $normalized); } + public function testNormalizeObjectWithMethodSameNameAsProperty() + { + $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())); + + $object = new ObjectWithMethodSameNameThanProperty(true); + + $this->assertSame(['shouldDoThing' => true], $normalizer->normalize($object)); + $this->assertSame(['shouldDoThing' => true], $normalizer->normalize($object, null, ['groups' => 'foo'])); + $this->assertSame([], $normalizer->normalize($object, null, ['groups' => 'bar'])); + } + + public function testIgnoreAttributeOnMethodWithSameNameAsProperty() + { + $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())); + + $object = new ObjectWithIgnoredMethodSameNameAsProperty('should_be_ignored', 'should_be_serialized'); + + $this->assertSame(['visible' => 'should_be_serialized'], $normalizer->normalize($object)); + } + + public function testIgnoreAttributeOnMethodWithSameNameAsPropertyWithGroups() + { + $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())); + + $object = new ObjectWithIgnoredMethodSameNameAsPropertyWithGroups('ignored', 'visible_default', 'visible_group'); + + // without groups - should include both visible properties + $this->assertSame(['visibleDefault' => 'visible_default', 'visibleGroup' => 'visible_group'], $normalizer->normalize($object)); + + // with groups - should only include group-specific property, ignored method should never appear + $this->assertSame(['visibleGroup' => 'visible_group'], $normalizer->normalize($object, null, ['groups' => ['group1']])); + } + /** * Priority of accessor methods is defined by the PropertyReadInfoExtractorInterface passed to the PropertyAccessor * component. By default ReflectionExtractor::$defaultAccessorPrefixes are used. @@ -1175,6 +1211,61 @@ public function testPrecedenceOfAccessorMethods() ], $normalizedSwappedHasserIsser); } + public function testIsserPrefersBaseNameWhenNoCollision() + { + $normalizer = new ObjectNormalizer(); + + $object = new ObjectWithIsPrefixedPropertyOnly(true); + + $this->assertSame(['published' => true], $normalizer->normalize($object)); + } + + public function testIsserKeepsPrefixWhenBaseNameCollides() + { + $normalizer = new ObjectNormalizer(); + + $object = new ObjectWithIsPrefixedPropertyAndPublishedGetter(true, 'live'); + + $this->assertEquals([ + 'published' => 'live', + 'isPublished' => true, + ], $normalizer->normalize($object)); + } + + public function testIsserKeepsPrefixWhenPublicPropertyCollidesWithoutGetter() + { + $normalizer = new ObjectNormalizer(); + + $object = new ObjectWithIsserAndPublicPropertyNoGetter(true, 'live'); + + // Both should appear: isPublished keeps prefix because $published property exists + $this->assertEquals([ + 'isPublished' => true, + 'published' => 'live', + ], $normalizer->normalize($object)); + } + + public function testIsserWithPublicPropertyCollision() + { + $normalizer = new ObjectNormalizer(); + + $object = new ObjectWithPublicPublishedPropertyAndIsser('live'); + + // The isser takes precedence over the public property - this documents existing behavior + $this->assertSame(['published' => true], $normalizer->normalize($object)); + } + + public function testIsserWithPrivatePropertyNoMethodNamedProperty() + { + $normalizer = new ObjectNormalizer(); + + $object = new ObjectWithPrivatePublishedAndIsser(true); + + // isPublished() should normalize to 'published', not 'isPublished' + // because there's no $isPublished property that would cause a collision + $this->assertSame(['published' => true], $normalizer->normalize($object)); + } + public function testDiscriminatorWithAllowExtraAttributesFalse() { // Discriminator type property should be allowed with allow_extra_attributes=false @@ -1192,6 +1283,51 @@ public function testDiscriminatorWithAllowExtraAttributesFalse() $this->assertInstanceOf(DiscriminatorDummyTypeA::class, $obj); } + public function testNameConverterWithWrongCaseAndAllowExtraAttributesFalse() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); + + $result = $normalizer->denormalize( + ['some_camel_case_property' => 1], + NameConverterTestDummy::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false] + ); + $this->assertSame(1, $result->someCamelCaseProperty); + + $this->expectException(ExtraAttributesException::class); + $this->expectExceptionMessage('someCamelCaseProperty'); + $normalizer->denormalize( + ['someCamelCaseProperty' => 1], + NameConverterTestDummy::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false] + ); + } + + public function testNameConverterWithWrongCaseAndAllowExtraAttributesTrue() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); + + $result = $normalizer->denormalize( + ['someCamelCaseProperty' => 999], + NameConverterTestDummy::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true] + ); + $this->assertSame(0, $result->someCamelCaseProperty); + + $result = $normalizer->denormalize( + ['some_camel_case_property' => 42], + NameConverterTestDummy::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true] + ); + $this->assertSame(42, $result->someCamelCaseProperty); + } + public function testNormalizeObjectWithGroupsAndIsPrefixedProperty() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); @@ -1202,10 +1338,35 @@ public function testNormalizeObjectWithGroupsAndIsPrefixedProperty() $object = new GroupDummyWithIsPrefixedProperty(); $normalizedWithoutGroups = $normalizer->normalize($object); - $this->assertArrayHasKey('isSomething', $normalizedWithoutGroups); + $this->assertArrayHasKey('something', $normalizedWithoutGroups); + + $normalizedWithGroups = $normalizer->normalize($object, null, [AbstractNormalizer::GROUPS => ['test']]); + $this->assertArrayHasKey('something', $normalizedWithGroups); + } + + public function testNormalizeObjectWithGroupsAndIsPrefixedPropertyWithCollision() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + + $object = new GroupDummyWithIsPrefixedPropertyAndPublishedGetter(); $normalizedWithGroups = $normalizer->normalize($object, null, [AbstractNormalizer::GROUPS => ['test']]); - $this->assertArrayHasKey('isSomething', $normalizedWithGroups); + + $this->assertArrayHasKey('isPublished', $normalizedWithGroups); + $this->assertArrayNotHasKey('published', $normalizedWithGroups); + } + + public function testSkipVoidNeverReturnTypeAccessors() + { + $obj = new VoidNeverReturnTypeDummy(); + $normalized = $this->normalizer->normalize($obj); + $this->assertArrayHasKey('normalProperty', $normalized); + $this->assertArrayNotHasKey('voidProperty', $normalized); + $this->assertArrayNotHasKey('neverProperty', $normalized); + $this->assertEquals('value', $normalized['normalProperty']); } } @@ -1676,3 +1837,174 @@ public function hasFoo() return 'hasFoo'; } } + +class ObjectWithIsPrefixedPropertyOnly +{ + public function __construct( + private bool $isPublished, + ) { + } + + public function isPublished(): bool + { + return $this->isPublished; + } +} + +class ObjectWithIsPrefixedPropertyAndPublishedGetter +{ + public function __construct( + private bool $isPublished, + private string $published, + ) { + } + + public function getPublished(): string + { + return $this->published; + } + + public function isPublished(): bool + { + return $this->isPublished; + } +} + +class GroupDummyWithIsPrefixedPropertyAndPublishedGetter +{ + private bool $isPublished = true; + private string $published = 'live'; + + #[Groups(['test'])] + public function isPublished(): bool + { + return $this->isPublished; + } + + public function getPublished(): string + { + return $this->published; + } +} + +class ObjectWithPublicPublishedPropertyAndIsser +{ + public string $published; + + public function __construct(string $published) + { + $this->published = $published; + } + + public function isPublished(): bool + { + return '' !== $this->published; + } +} + +class ObjectWithPrivatePublishedAndIsser +{ + public function __construct( + private bool $published, + ) { + } + + public function isPublished(): bool + { + return $this->published; + } +} + +class ObjectWithIsserAndPublicPropertyNoGetter +{ + public string $published; + + public function __construct( + private bool $isPublished, + string $published, + ) { + $this->published = $published; + } + + public function isPublished(): bool + { + return $this->isPublished; + } +} + +class ObjectWithMethodSameNameThanProperty +{ + public function __construct( + private $shouldDoThing, + ) { + } + + #[Groups(['Default', 'foo'])] + public function shouldDoThing() + { + return $this->shouldDoThing; + } +} + +class ObjectWithIgnoredMethodSameNameAsProperty +{ + public string $visible; + + private $ignored; + + public function __construct(string $ignored, string $visible) + { + $this->ignored = $ignored; + $this->visible = $visible; + } + + #[Ignore] + public function ignored() + { + return $this->ignored; + } +} + +class ObjectWithIgnoredMethodSameNameAsPropertyWithGroups +{ + public string $visibleDefault; + public string $visibleGroup; + + private $ignored; + + public function __construct(string $ignored, string $visibleDefault, string $visibleGroup) + { + $this->ignored = $ignored; + $this->visibleDefault = $visibleDefault; + $this->visibleGroup = $visibleGroup; + } + + #[Ignore] + public function ignored() + { + return $this->ignored; + } + + #[Groups(['group1'])] + public function visibleGroup() + { + return $this->visibleGroup; + } +} + +class NameConverterTestDummy +{ + public function __construct( + public readonly int $someCamelCaseProperty = 0, + ) { + } +} + +class NameConverterTestDummyMultiple +{ + public function __construct( + public readonly int $someCamelCaseProperty = 0, + public readonly int $anotherProperty = 0, + ) { + } +} diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 8674c767670..af89b217596 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -74,7 +74,7 @@ protected function setUp(): void private function createNormalizer(array $defaultContext = []): void { - $this->serializer = $this->createMock(SerializerInterface::class); + $this->serializer = $this->createStub(SerializerInterface::class); $this->normalizer = new PropertyNormalizer(null, null, null, null, null, $defaultContext); $this->normalizer->setSerializer($this->serializer); } @@ -455,7 +455,7 @@ public function testDenormalizeShouldIgnoreStaticProperty() public function testUnableToNormalizeObjectAttribute() { - $serializer = $this->createMock(SerializerInterface::class); + $serializer = $this->createStub(SerializerInterface::class); $this->normalizer->setSerializer($serializer); $obj = new PropertyDummy(); diff --git a/Tests/Normalizer/TranslatableNormalizerTest.php b/Tests/Normalizer/TranslatableNormalizerTest.php index 3499521b65c..ced0e663929 100644 --- a/Tests/Normalizer/TranslatableNormalizerTest.php +++ b/Tests/Normalizer/TranslatableNormalizerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Serializer\Normalizer\TranslatableNormalizer; use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorTrait; class TranslatableNormalizerTest extends TestCase { @@ -22,7 +23,7 @@ class TranslatableNormalizerTest extends TestCase protected function setUp(): void { - $this->normalizer = new TranslatableNormalizer($this->createMock(TranslatorInterface::class)); + $this->normalizer = new TranslatableNormalizer(new IdentityTranslator()); } public function testSupportsNormalization() @@ -43,7 +44,7 @@ public function testNormalize() public function testNormalizeWithNormalizationLocalePassedInConstructor() { $normalizer = new TranslatableNormalizer( - $this->createMock(TranslatorInterface::class), + new IdentityTranslator(), ['translatable_normalization_locale' => 'es'], ); $message = new TestMessage(); @@ -61,3 +62,8 @@ public function trans(TranslatorInterface $translator, ?string $locale = null): return 'key_'.($locale ?? 'null'); } } + +class IdentityTranslator implements TranslatorInterface +{ + use TranslatorTrait; +} diff --git a/Tests/Normalizer/UnwrappinDenormalizerTest.php b/Tests/Normalizer/UnwrappingDenormalizerTest.php similarity index 62% rename from Tests/Normalizer/UnwrappinDenormalizerTest.php rename to Tests/Normalizer/UnwrappingDenormalizerTest.php index 59ddd6da65a..80d4a580735 100644 --- a/Tests/Normalizer/UnwrappinDenormalizerTest.php +++ b/Tests/Normalizer/UnwrappingDenormalizerTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; use Symfony\Component\Serializer\Serializer; @@ -20,23 +19,16 @@ /** * @author Eduard Bulava */ -class UnwrappinDenormalizerTest extends TestCase +class UnwrappingDenormalizerTest extends TestCase { - private UnwrappingDenormalizer $denormalizer; - private MockObject&Serializer $serializer; - - protected function setUp(): void - { - $this->serializer = $this->createMock(Serializer::class); - $this->denormalizer = new UnwrappingDenormalizer(); - $this->denormalizer->setSerializer($this->serializer); - } - public function testSupportsNormalization() { - $this->assertTrue($this->denormalizer->supportsDenormalization([], 'stdClass', 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]'])); - $this->assertFalse($this->denormalizer->supportsDenormalization([], 'stdClass', 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]', 'unwrapped' => true])); - $this->assertFalse($this->denormalizer->supportsDenormalization([], 'stdClass', 'any', [])); + $denormalizer = new UnwrappingDenormalizer(); + $denormalizer->setSerializer($this->createStub(Serializer::class)); + + $this->assertTrue($denormalizer->supportsDenormalization([], 'stdClass', 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]'])); + $this->assertFalse($denormalizer->supportsDenormalization([], 'stdClass', 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]', 'unwrapped' => true])); + $this->assertFalse($denormalizer->supportsDenormalization([], 'stdClass', 'any', [])); } public function testDenormalize() @@ -46,12 +38,15 @@ public function testDenormalize() $expected->bar = 'bar'; $expected->setFoo('foo'); - $this->serializer->expects($this->exactly(1)) + $serializer = $this->createMock(Serializer::class); + $serializer->expects($this->exactly(1)) ->method('denormalize') ->with(['foo' => 'foo', 'bar' => 'bar', 'baz' => true]) ->willReturn($expected); + $denormalizer = new UnwrappingDenormalizer(); + $denormalizer->setSerializer($serializer); - $result = $this->denormalizer->denormalize( + $result = $denormalizer->denormalize( ['data' => ['foo' => 'foo', 'bar' => 'bar', 'baz' => true]], ObjectDummy::class, 'any', @@ -65,12 +60,15 @@ public function testDenormalize() public function testDenormalizeInvalidPath() { - $this->serializer->expects($this->exactly(1)) + $serializer = $this->createMock(Serializer::class); + $serializer->expects($this->exactly(1)) ->method('denormalize') ->with(null) ->willReturn(new ObjectDummy()); + $denormalizer = new UnwrappingDenormalizer(); + $denormalizer->setSerializer($serializer); - $obj = $this->denormalizer->denormalize( + $obj = $denormalizer->denormalize( ['data' => ['foo' => 'foo', 'bar' => 'bar', 'baz' => true]], ObjectDummy::class, 'any', diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 6f9e65ae04f..c9b1bd7ec5c 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -106,7 +106,7 @@ public function testItThrowsExceptionOnInvalidEncoder() public function testNormalizeNoMatch() { - $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + $serializer = new Serializer([$this->createStub(NormalizerInterface::class)]); $this->expectException(UnexpectedValueException::class); @@ -138,7 +138,7 @@ public function testNormalizeOnDenormalizer() public function testDenormalizeNoMatch() { - $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + $serializer = new Serializer([$this->createStub(NormalizerInterface::class)]); $this->expectException(UnexpectedValueException::class); @@ -174,13 +174,13 @@ public function testCustomNormalizerCanNormalizeCollectionsAndScalar() public function testNormalizeWithSupportOnData() { - $normalizer1 = $this->createMock(NormalizerInterface::class); + $normalizer1 = $this->createStub(NormalizerInterface::class); $normalizer1->method('getSupportedTypes')->willReturn(['*' => false]); $normalizer1->method('supportsNormalization') ->willReturnCallback(fn ($data, $format) => isset($data->test)); $normalizer1->method('normalize')->willReturn('test1'); - $normalizer2 = $this->createMock(NormalizerInterface::class); + $normalizer2 = $this->createStub(NormalizerInterface::class); $normalizer2->method('getSupportedTypes')->willReturn(['*' => false]); $normalizer2->method('supportsNormalization') ->willReturn(true); @@ -197,13 +197,13 @@ public function testNormalizeWithSupportOnData() public function testDenormalizeWithSupportOnData() { - $denormalizer1 = $this->createMock(DenormalizerInterface::class); + $denormalizer1 = $this->createStub(DenormalizerInterface::class); $denormalizer1->method('getSupportedTypes')->willReturn(['*' => false]); $denormalizer1->method('supportsDenormalization') ->willReturnCallback(fn ($data, $type, $format) => isset($data['test1'])); $denormalizer1->method('denormalize')->willReturn('test1'); - $denormalizer2 = $this->createMock(DenormalizerInterface::class); + $denormalizer2 = $this->createStub(DenormalizerInterface::class); $denormalizer2->method('getSupportedTypes')->willReturn(['*' => false]); $denormalizer2->method('supportsDenormalization') ->willReturn(true); diff --git a/composer.json b/composer.json index 286337b31f5..8b22b980d19 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,8 @@ }, "conflict": { "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0" + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/property-info": "<7.3" }, "autoload": { "psr-4": { "Symfony\\Component\\Serializer\\": "" },