diff --git a/.gitattributes b/.gitattributes index d1570aff1cd7..d25fa38fe3c9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,8 @@ /src/Symfony/Component/Notifier/Bridge export-ignore /src/Symfony/Component/Runtime export-ignore /src/Symfony/Component/Translation/Bridge export-ignore + +# Keep generated files from displaying in diffs by default +# https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github /src/Symfony/Component/Intl/Resources/data/*/* linguist-generated=true +/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/*/ExpectedNormalizer/* linguist-generated=true diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 6f53e57f069c..65123b211084 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -70,6 +70,7 @@ class UnusedTagsPass implements CompilerPassInterface 'monolog.logger', 'notifier.channel', 'property_info.access_extractor', + 'property_info.constructor_argument_type_extractor', 'property_info.initializable_extractor', 'property_info.list_extractor', 'property_info.type_extractor', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 702b4f610907..5bac9212d57c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1124,6 +1124,40 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->defaultValue([]) ->prototype('variable')->end() ->end() + ->arrayNode('auto_normalizer') + ->addDefaultsIfNotSet() + ->fixXmlConfig('path') + ->children() + ->arrayNode('paths') + ->validate() + ->ifTrue(function ($data): bool { + foreach ($data as $key => $value) { + if (!\is_string($key)) { + return true; + } + if (!\is_string($value)) { + return true; + } + } + + return false; + }) + ->thenInvalid('The value must be an array with keys and values. Keys should be the start of a namespace and the values should be a file path.') + ->end() + ->info('Paths where we store classes we want to automatically create normalizers for.') + ->normalizeKeys(false) + ->defaultValue([]) + ->example(['App\\Model' => 'src/Model', 'App\\Entity' => 'src/Entity']) + ->useAttributeAsKey('name') + ->variablePrototype() + ->validate() + ->ifTrue(fn ($value): bool => !\is_string($value)) + ->thenInvalid('The value must be a string representing a path relative to the project root.') + ->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9c9446fb03ce..269a431d9cd6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -390,6 +390,10 @@ public function load(array $configs, ContainerBuilder $container): void if ($propertyInfoEnabled) { $this->registerPropertyInfoConfiguration($container, $loader); + } else { + // Remove services depending on PropertyInfo + $container->removeDefinition('serializer.auto_normalizer.definition_extractor'); + $container->removeDefinition('serializer.custom_normalizer_helper'); } if ($this->readConfigEnabled('lock', $container, $config['lock'])) { @@ -1861,6 +1865,8 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.normalizer.translatable'); } + $container->getDefinition('serializer.custom_normalizer_helper')->replaceArgument(2, $config['auto_normalizer']['paths']); + $serializerLoaders = []; if (isset($config['enable_attributes']) && $config['enable_attributes']) { $attributeLoader = new Definition(AttributeLoader::class); @@ -1940,12 +1946,14 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, ) { $definition = $container->register('property_info.phpstan_extractor', PhpStanExtractor::class); $definition->addTag('property_info.type_extractor', ['priority' => -1000]); + $definition->addTag('property_info.constructor_argument_type_extractor'); } if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'], true)) { $definition = $container->register('property_info.php_doc_extractor', PhpDocExtractor::class); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); + $definition->addTag('property_info.constructor_argument_type_extractor'); } if ($container->getParameter('kernel.debug')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 26784bec367d..6c69612a6bae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -150,8 +150,8 @@ public function build(ContainerBuilder $container): void $this->addCompilerPassIfExists($container, TranslationExtractorPass::class); $this->addCompilerPassIfExists($container, TranslationDumperPass::class); $container->addCompilerPass(new FragmentRendererPass()); - $this->addCompilerPassIfExists($container, SerializerPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); + $this->addCompilerPassIfExists($container, SerializerPass::class); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); $container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32); $container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php index 90587839d54c..6f84eef1d877 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorAggregate; +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; @@ -45,8 +47,14 @@ ->tag('property_info.type_extractor', ['priority' => -1002]) ->tag('property_info.access_extractor', ['priority' => -1000]) ->tag('property_info.initializable_extractor', ['priority' => -1000]) + ->tag('property_info.constructor_argument_type_extractor') ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') + + ->set('property_info.constructor_argument_type_extractor_aggregate', ConstructorArgumentTypeExtractorAggregate::class) + ->args([tagged_iterator('property_info.constructor_argument_type_extractor')]) + ->alias(ConstructorArgumentTypeExtractorInterface::class, 'property_info.constructor_argument_type_extractor_aggregate') + ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 799cfb2900f9..dc455d49620a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -16,7 +16,14 @@ use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\Serializer\Builder\DefinitionExtractor; +use Symfony\Component\Serializer\Builder\NormalizerBuilder; +use Symfony\Component\Serializer\DependencyInjection\CustomNormalizerHelper; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; @@ -169,6 +176,25 @@ service('serializer.mapping.cache.symfony'), ]) + // Auto Normalizer Builder + ->set('serializer.auto_normalizer.builder', NormalizerBuilder::class) + ->set('serializer.auto_normalizer.definition_extractor', DefinitionExtractor::class) + ->args([ + service(PropertyInfoExtractorInterface::class), + service(PropertyReadInfoExtractorInterface::class), + service(PropertyWriteInfoExtractorInterface::class), + service(ConstructorArgumentTypeExtractorInterface::class), + ]) + + ->set('serializer.custom_normalizer_helper', CustomNormalizerHelper::class) + ->args([ + service('serializer.auto_normalizer.builder'), + service('serializer.auto_normalizer.definition_extractor'), + [], + param('kernel.project_dir'), + service('logger')->nullOnInvalid(), + ]) + // Encoders ->set('serializer.encoder.xml', XmlEncoder::class) ->tag('serializer.encoder') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index d56cfa90d7f4..4672735b2b49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -615,6 +615,7 @@ protected static function getBundleDefaultConfig() 'enabled' => true, 'enable_attributes' => !class_exists(FullStack::class), 'mapping' => ['paths' => []], + 'auto_normalizer' => ['paths' => []], ], 'property_access' => [ 'enabled' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index b5d8061e4d0a..481a71473eec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -64,6 +64,7 @@ 'serializer' => [ 'enabled' => true, 'enable_attributes' => true, + 'auto_normalizer' => ['paths'=>[]], 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', 'circular_reference_handler' => 'my.circular.reference.handler', 'max_depth_handler' => 'my.max.depth.handler', diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 6e0a2ff449de..f234d4631e71 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * Introduce `PropertyDocBlockExtractorInterface` to extract a property's doc block + * Make ConstructorArgumentTypeExtractorInterface non-internal + * Add `ConstructorArgumentTypeExtractorAggregate` to aggregate multiple `ConstructorArgumentTypeExtractorInterface` implementations 6.4 --- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorAggregate.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorAggregate.php new file mode 100644 index 000000000000..408871f40edc --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorAggregate.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +/** + * @author Tobias Nyholm + */ +class ConstructorArgumentTypeExtractorAggregate implements ConstructorArgumentTypeExtractorInterface +{ + /** + * @param iterable $extractors + */ + public function __construct( + private readonly iterable $extractors = [], + ) { + } + + public function getTypesFromConstructor(string $class, string $property): ?array + { + $output = []; + foreach ($this->extractors as $extractor) { + $value = $extractor->getTypesFromConstructor($class, $property); + if (null !== $value) { + $output[] = $value; + } + } + + if ([] === $output) { + return null; + } + + return array_merge([], ...$output); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php index cbde902e9801..666aaba7ffb5 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -17,8 +17,6 @@ * Infers the constructor argument type. * * @author Dmitrii Poddubnyi - * - * @internal */ interface ConstructorArgumentTypeExtractorInterface { diff --git a/src/Symfony/Component/Serializer/.editorconfig b/src/Symfony/Component/Serializer/.editorconfig new file mode 100644 index 000000000000..3ae2aa3ddb06 --- /dev/null +++ b/src/Symfony/Component/Serializer/.editorconfig @@ -0,0 +1,9 @@ +# Ignore paths +[Tests/CodeGenerator/Fixtures/**] +trim_trailing_whitespace = false + +[Tests/Fixtures/CustomNormalizer/**/ExpectedNormalizer/**] +trim_trailing_whitespace = false +insert_final_newline = false +indent_size = unset +indent_style = unset diff --git a/src/Symfony/Component/Serializer/.gitignore b/src/Symfony/Component/Serializer/.gitignore index c49a5d8df5c6..525face3fb3e 100644 --- a/src/Symfony/Component/Serializer/.gitignore +++ b/src/Symfony/Component/Serializer/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock phpunit.xml +Tests/_output diff --git a/src/Symfony/Component/Serializer/Annotation/Serializable.php b/src/Symfony/Component/Serializer/Annotation/Serializable.php new file mode 100644 index 000000000000..5df0631b00a3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/Serializable.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Annotation; + +class_exists(\Symfony\Component\Serializer\Attribute\Serializable::class); + +if (false) { + #[\Attribute(\Attribute::TARGET_CLASS)] + class Serializable extends \Symfony\Component\Serializer\Attribute\Serializable + { + } +} diff --git a/src/Symfony/Component/Serializer/Attribute/Serializable.php b/src/Symfony/Component/Serializer/Attribute/Serializable.php new file mode 100644 index 000000000000..cb8d95e74623 --- /dev/null +++ b/src/Symfony/Component/Serializer/Attribute/Serializable.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Attribute; + +/** + * Classes with this attribute will get a custom normalizer to improve speed when + * serializing/deserializing. + * + * @author Tobias Nyholm + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Serializable +{ +} + +if (!class_exists(\Symfony\Component\Serializer\Annotation\Serializable::class, false)) { + class_alias(Serializable::class, \Symfony\Component\Serializer\Annotation\Serializable::class); +} diff --git a/src/Symfony/Component/Serializer/Builder/BuildResult.php b/src/Symfony/Component/Serializer/Builder/BuildResult.php new file mode 100644 index 000000000000..badd3e62b4e1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Builder/BuildResult.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Builder; + +/** + * @author Tobias Nyholm + * + * @experimental in 7.1 + */ +class BuildResult +{ + public function __construct( + // The full file location where the class is stored + public readonly string $filePath, + // Just the class name + public readonly string $className, + // Class name with namespace + public readonly string $classNs, + ) { + } + + public function loadClass(): void + { + require_once $this->filePath; + } +} diff --git a/src/Symfony/Component/Serializer/Builder/ClassDefinition.php b/src/Symfony/Component/Serializer/Builder/ClassDefinition.php new file mode 100644 index 000000000000..323de5ce4bc9 --- /dev/null +++ b/src/Symfony/Component/Serializer/Builder/ClassDefinition.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Builder; + +/** + * This contains all necessary information about a class to create a custom Normalizer. + * It has an array of PropertyDefinitions. + * + * @author Tobias Nyholm + * + * @experimental in 7.1 + */ +class ClassDefinition +{ + public const CONSTRUCTOR_NONE = 'none'; + public const CONSTRUCTOR_NON_PUBLIC = 'non_public'; + public const CONSTRUCTOR_PUBLIC = 'public'; + + private string $sourceClassName; + private string $namespaceAndClass; + private string $newNamespace; + private string $newClassName; + private string $constructorType = self::CONSTRUCTOR_NONE; + + /** + * @var PropertyDefinition[] + */ + private array $definitions = []; + + public function __construct(string $namespaceAndClass, string $newClassName, string $newNamespace) + { + $this->namespaceAndClass = $namespaceAndClass; + $this->newNamespace = $newNamespace; + $this->newClassName = $newClassName; + $this->sourceClassName = substr($namespaceAndClass, strrpos($namespaceAndClass, '\\') + 1); + } + + public function getSourceClassName(): string + { + return $this->sourceClassName; + } + + public function getNamespaceAndClass(): string + { + return $this->namespaceAndClass; + } + + /** + * @return PropertyDefinition[] + */ + public function getDefinitions(): array + { + return $this->definitions; + } + + public function getDefinition(string $propertyName): ?PropertyDefinition + { + return $this->definitions[$propertyName] ?? null; + } + + public function addDefinition(PropertyDefinition $definition): void + { + $this->definitions[$definition->getPropertyName()] = $definition; + } + + public function getConstructorType(): string + { + return $this->constructorType; + } + + public function setConstructorType(string $constructorType): void + { + $this->constructorType = $constructorType; + } + + public function getNewNamespace(): string + { + return $this->newNamespace; + } + + public function getNewClassName(): string + { + return $this->newClassName; + } + + /** + * @return PropertyDefinition[] + */ + public function getConstructorArguments(): array + { + $arguments = []; + foreach ($this->definitions as $def) { + $order = $def->getConstructorArgumentOrder(); + if (null === $order) { + continue; + } + $arguments[$order] = $def; + } + + return $arguments; + } +} diff --git a/src/Symfony/Component/Serializer/Builder/DefinitionExtractor.php b/src/Symfony/Component/Serializer/Builder/DefinitionExtractor.php new file mode 100644 index 000000000000..f657df2ea798 --- /dev/null +++ b/src/Symfony/Component/Serializer/Builder/DefinitionExtractor.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Builder; + +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Take in a class and extract the definition. + * + * @author Tobias Nyholm + * + * @experimental in 7.1 + */ +class DefinitionExtractor +{ + public function __construct( + private PropertyInfoExtractorInterface $propertyInfo, + private PropertyReadInfoExtractorInterface $propertyReadInfoExtractor, + private PropertyWriteInfoExtractorInterface $propertyWriteInfoExtractor, + private ConstructorArgumentTypeExtractorInterface $constructorArgumentTypeExtractor, + ) { + } + + public function getDefinition(string $classNs): ClassDefinition + { + $className = str_replace('\\', '_', ltrim($classNs, '\\')); + $definition = new ClassDefinition($classNs, $className, 'Symfony\\Serializer\\Normalizer'); + $this->extractProperties($definition); + + return $definition; + } + + private function extractProperties(ClassDefinition $classDefinition): void + { + $classNs = $classDefinition->getNamespaceAndClass(); + + /* + * Extract constructor. + */ + $reflectionClass = new \ReflectionClass($classNs); + $constructor = $reflectionClass->getConstructor(); + if (null === $constructor) { + $classDefinition->setConstructorType(ClassDefinition::CONSTRUCTOR_NONE); + } elseif (!$constructor->isPublic()) { + $classDefinition->setConstructorType(ClassDefinition::CONSTRUCTOR_NON_PUBLIC); + } else { + $classDefinition->setConstructorType(ClassDefinition::CONSTRUCTOR_PUBLIC); + + foreach ($constructor->getParameters() as $i => $parameter) { + // We assume the constructor parameter name is the same as the property + $definition = $this->createOrGetDefinition($classDefinition, $parameter->getName()); + $definition->setConstructorArgumentOrder($i); + if ($parameter->isDefaultValueAvailable()) { + $definition->setConstructorDefaultValue($parameter->getDefaultValue()); + } + $types = $this->constructorArgumentTypeExtractor->getTypesFromConstructor($classNs, $parameter->getName()); + $this->parseTypes($definition, $types); + } + } + + /* + * Extract properties + */ + foreach ($this->propertyInfo->getProperties($classNs) ?? [] as $property) { + $definition = $this->createOrGetDefinition($classDefinition, $property); + $definition->setIsReadable($this->propertyInfo->isReadable($classNs, $property)); + $definition->setIsWriteable($this->propertyInfo->isWritable($classNs, $property)); + + $types = $this->propertyInfo->getTypes($classNs, $property); + $this->parseTypes($definition, $types); + + $info = $this->propertyReadInfoExtractor->getReadInfo($classNs, $property); + if (null !== $info && PropertyReadInfo::TYPE_METHOD === $info->getType()) { + $definition->setGetterName($info->getName()); + } + + $info = $this->propertyWriteInfoExtractor->getWriteInfo($classNs, $property); + if (null !== $info && PropertyReadInfo::TYPE_METHOD === $info->getType()) { + $definition->setSetterName($info->getName()); + } + } + } + + private function createOrGetDefinition(ClassDefinition $classDefinition, string $property): PropertyDefinition + { + $definition = $classDefinition->getDefinition($property); + if (null === $definition) { + $definition = new PropertyDefinition($property); + $classDefinition->addDefinition($definition); + } + + return $definition; + } + + /** + * @param Type[]|null $types + */ + private function parseTypes(PropertyDefinition $definition, ?array $types): void + { + $isCollection = false; + $targetClasses = []; + $builtInTypes = []; + + if (null !== $types) { + foreach ($types as $type) { + $this->parseType($type, $builtInTypes, $isCollection, $targetClasses); + } + } + + // Flip and remove empty values + $targetClasses = array_keys($targetClasses); + $targetClasses = array_filter($targetClasses); + $definition->setNonPrimitiveTypes($targetClasses); + $definition->setScalarTypes($builtInTypes); + $definition->setIsCollection($isCollection); + } + + private function parseType(Type $type, array &$builtInTypes, bool &$isCollection, array &$targetClasses): void + { + $builtinType = $type->getBuiltinType(); + + if (\in_array($builtinType, ['bool', 'int', 'float', 'string'])) { + $builtInTypes[] = $builtinType; + } + $isCollection = $isCollection || $type->isCollection(); + if (Type::BUILTIN_TYPE_OBJECT === $builtinType) { + $targetClasses[$type->getClassName()] = true; + } elseif ($type->isCollection()) { + foreach ($type->getCollectionValueTypes() as $collectionType) { + $this->parseType($collectionType, $builtInTypes, $isCollection, $targetClasses); + } + } + } +} diff --git a/src/Symfony/Component/Serializer/Builder/NormalizerBuilder.php b/src/Symfony/Component/Serializer/Builder/NormalizerBuilder.php new file mode 100644 index 000000000000..ee778bbe6d62 --- /dev/null +++ b/src/Symfony/Component/Serializer/Builder/NormalizerBuilder.php @@ -0,0 +1,675 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Builder; + +use PhpParser\Builder\Class_; +use PhpParser\Builder\Namespace_; +use PhpParser\BuilderFactory; +use PhpParser\Node; +use PhpParser\ParserFactory; +use PhpParser\PrettyPrinter; +use Symfony\Component\Serializer\Exception\DenormalizingUnionFailedException; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * The main class to create a new Normalizer from a ClassDefinition. + * + * @author Tobias Nyholm + * + * @experimental in 7.1 + */ +class NormalizerBuilder +{ + private PrettyPrinter\Standard $printer; + private BuilderFactory $factory; + + public function __construct() + { + if (!class_exists(ParserFactory::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the "nikic/php-parser" package is not installed. Try running "composer require nikic/php-parser".', static::class)); + } + + $this->factory = new BuilderFactory(); + $this->printer = new PrettyPrinter\Standard(); + } + + public function build(ClassDefinition $definition, string $outputDir): BuildResult + { + $namespace = $this->factory->namespace($definition->getNewNamespace()) + ->addStmt($this->factory->use($definition->getNamespaceAndClass())); + + $class = $this->factory->class($definition->getNewClassName()); + $this->addRequiredMethods($definition, $namespace, $class); + $this->addNormailizeMethod($definition, $namespace, $class); + $this->addDenormailizeMethod($definition, $namespace, $class); + + // Add class to namespace + $namespace->addStmt($class); + $node = $namespace->getNode(); + + // Print + @mkdir($outputDir, 0777, true); + $outputFile = $outputDir.'/'.$definition->getNewClassName().'.php'; + file_put_contents($outputFile, $this->printer->prettyPrintFile([$node])); + + return new BuildResult( + $outputFile, + $definition->getNewClassName(), + sprintf('%s\\%s', $definition->getNewNamespace(), $definition->getNewClassName()) + ); + } + + /** + * Generate a private helper class to normalize subtypes. + */ + private function generateNormalizeChildMethod(Namespace_ $namespace, Class_ $class): void + { + $namespace->addStmt($this->factory->use(NormalizerAwareInterface::class)); + $class->implement('NormalizerAwareInterface'); + $class->addStmt($this->factory->property('normalizer') + ->makePrivate() + ->setType('null|NormalizerInterface') + ->setDefault(null)); + + // public function setNormalizer(NormalizerInterface $normalizer): void; + $class->addStmt($this->factory->method('setNormalizer') + ->makePublic() + ->addParam($this->factory->param('normalizer')->setType('NormalizerInterface')) + ->setReturnType('void') + ->addStmt( + new Node\Stmt\Expression( + new Node\Expr\Assign( + $this->factory->propertyFetch(new Node\Expr\Variable('this'), 'normalizer'), + new Node\Expr\Variable('normalizer') + ) + ) + ) + ); + + // private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed; + $class->addStmt($this->factory->method('normalizeChild') + ->makePrivate() + ->addParam($this->factory->param('object')->setType('mixed')) + ->addParam($this->factory->param('format')->setType('?string')) + ->addParam($this->factory->param('context')->setType('array')) + ->addParam($this->factory->param('canBeIterable')->setType('bool')) + ->setReturnType('mixed') + ->addStmts([ + new Node\Stmt\If_( + new Node\Expr\BinaryOp\BooleanOr( + $this->factory->funcCall(new Node\Name('is_scalar'), [ + new Node\Arg(new Node\Expr\Variable('object')), + ]), + new Node\Expr\BinaryOp\Identical( + new Node\Expr\ConstFetch(new Node\Name('null')), + new Node\Expr\Variable('object') + ) + ), + [ + 'stmts' => [ + new Node\Stmt\Return_(new Node\Expr\Variable('object')), + ], + ] + ), + // new line + new Node\Stmt\If_( + new Node\Expr\BinaryOp\BooleanAnd( + new Node\Expr\Variable('canBeIterable'), + $this->factory->funcCall(new Node\Name('is_iterable'), [ + new Node\Arg(new Node\Expr\Variable('object')), + ]) + ), + [ + 'stmts' => [ + new Node\Stmt\Return_( + new Node\Expr\FuncCall( + new Node\Name('array_map'), + [ + new Node\Arg( + new Node\Expr\ArrowFunction([ + 'params' => [new Node\Param(new Node\Expr\Variable('item'))], + 'expr' => $this->factory->methodCall( + new Node\Expr\Variable('this'), + 'normalizeChild', + [ + new Node\Arg(new Node\Expr\Variable('item')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name('true'))), + ] + ), + ]) + ), + new Node\Arg(new Node\Expr\Variable('object')), + ] + ) + ), + ], + ] + ), + // new line + new Node\Stmt\Return_( + $this->factory->methodCall( + $this->factory->propertyFetch(new Node\Expr\Variable('this'), 'normalizer'), + 'normalize', + [ + new Node\Arg(new Node\Expr\Variable('object')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + ] + ) + ), + ]) + ); + } + + /** + * Generate a private helper class to de-normalize subtypes. + */ + private function generateDenormalizeChildMethod(Namespace_ $namespace, Class_ $class): void + { + $namespace->addStmt($this->factory->use(DenormalizingUnionFailedException::class)); + $namespace->addStmt($this->factory->use(DenormalizerAwareInterface::class)); + $class->implement('DenormalizerAwareInterface'); + $class->addStmt($this->factory->property('denormalizer') + ->makePrivate() + ->setType('null|DenormalizerInterface') + ->setDefault(null)); + + // public function setNormalizer(NormalizerInterface $normalizer): void; + $class->addStmt($this->factory->method('setDenormalizer') + ->makePublic() + ->addParam($this->factory->param('denormalizer')->setType('DenormalizerInterface')) + ->setReturnType('void') + ->addStmt( + new Node\Stmt\Expression( + new Node\Expr\Assign( + $this->factory->propertyFetch(new Node\Expr\Variable('this'), 'denormalizer'), + new Node\Expr\Variable('denormalizer') + ) + ) + ) + ); + + // private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed; + $class->addStmt($this->factory->method('denormalizeChild') + ->makePrivate() + ->addParam($this->factory->param('data')->setType('mixed')) + ->addParam($this->factory->param('type')->setType('string')) + ->addParam($this->factory->param('format')->setType('?string')) + ->addParam($this->factory->param('context')->setType('array')) + ->addParam($this->factory->param('canBeIterable')->setType('bool')) + ->setReturnType('mixed') + ->addStmts([ + new Node\Stmt\If_( + new Node\Expr\BinaryOp\BooleanOr( + $this->factory->funcCall(new Node\Name('is_scalar'), [ + new Node\Arg(new Node\Expr\Variable('data')), + ]), + new Node\Expr\BinaryOp\Identical( + new Node\Expr\ConstFetch(new Node\Name('null')), + new Node\Expr\Variable('data') + ) + ), + [ + 'stmts' => [ + new Node\Stmt\Return_(new Node\Expr\Variable('data')), + ], + ] + ), + // new line + new Node\Stmt\If_( + new Node\Expr\BinaryOp\BooleanAnd( + new Node\Expr\Variable('canBeIterable'), + $this->factory->funcCall(new Node\Name('is_iterable'), [ + new Node\Arg(new Node\Expr\Variable('data')), + ]) + ), + [ + 'stmts' => [ + new Node\Stmt\Return_( + new Node\Expr\FuncCall( + new Node\Name('array_map'), + [ + new Node\Arg( + new Node\Expr\ArrowFunction([ + 'params' => [new Node\Param(new Node\Expr\Variable('item'))], + 'expr' => $this->factory->methodCall( + new Node\Expr\Variable('this'), + 'denormalizeChild', + [ + new Node\Arg(new Node\Expr\Variable('item')), + new Node\Arg(new Node\Expr\Variable('type')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name('true'))), + ] + ), + ]) + ), + new Node\Arg(new Node\Expr\Variable('data')), + ] + ) + ), + ], + ] + ), + // new line + new Node\Stmt\Return_( + $this->factory->methodCall( + $this->factory->propertyFetch(new Node\Expr\Variable('this'), 'denormalizer'), + 'denormalize', + [ + new Node\Arg(new Node\Expr\Variable('data')), + new Node\Arg(new Node\Expr\Variable('type')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + ] + ) + ), + ]) + ); + } + + /** + * Add methods required by NormalizerInterface and DenormalizerInterface. + */ + private function addRequiredMethods(ClassDefinition $definition, Namespace_ $namespace, Class_ $class): void + { + $namespace + ->addStmt($this->factory->use(NormalizerInterface::class)) + ->addStmt($this->factory->use(DenormalizerInterface::class)); + + $class->implement('NormalizerInterface', 'DenormalizerInterface'); + + // public function getSupportedTypes(?string $format): array; + $class->addStmt($this->factory->method('getSupportedTypes') + ->makePublic() + ->addParam($this->factory->param('format')->setType('?string')) + ->setReturnType('array') + ->addStmt(new Node\Stmt\Return_(new Node\Expr\Array_([ + new Node\Expr\ArrayItem( + new Node\Expr\ConstFetch(new Node\Name('true')), + new Node\Expr\ClassConstFetch(new Node\Name($definition->getSourceClassName()), 'class') + ), + ]))) + ); + + // public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool; + $class->addStmt($this->factory->method('supportsNormalization') + ->makePublic() + ->addParam($this->factory->param('data')->setType('mixed')) + ->addParam($this->factory->param('format')->setType('string')->setDefault(null)) + ->addParam($this->factory->param('context')->setType('array')->setDefault([])) + ->setReturnType('bool') + ->addStmt(new Node\Stmt\Return_(new Node\Expr\Instanceof_( + new Node\Expr\Variable('data'), + new Node\Name($definition->getSourceClassName()) + ))) + ); + + // public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool; + $class->addStmt($this->factory->method('supportsDenormalization') + ->makePublic() + ->addParam($this->factory->param('data')->setType('mixed')) + ->addParam($this->factory->param('type')->setType('string')) + ->addParam($this->factory->param('format')->setType('string')->setDefault(null)) + ->addParam($this->factory->param('context')->setType('array')->setDefault([])) + ->setReturnType('bool') + ->addStmt(new Node\Stmt\Return_(new Node\Expr\BinaryOp\Identical( + new Node\Expr\Variable('type'), + new Node\Expr\ClassConstFetch(new Node\Name($definition->getSourceClassName()), 'class') + ))) + ); + } + + private function addDenormailizeMethod(ClassDefinition $definition, Namespace_ $namespace, Class_ $class): void + { + $needsChildDenormalizer = false; + $body = [ + new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('data'), + new Node\Expr\Cast\Array_(new Node\Expr\Variable('data')) + )), + ]; + + if (ClassDefinition::CONSTRUCTOR_NONE === $definition->getConstructorType()) { + $body[] = new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('output'), + new Node\Expr\New_( + new Node\Name($definition->getSourceClassName()) + ) + )); + } elseif (ClassDefinition::CONSTRUCTOR_PUBLIC !== $definition->getConstructorType()) { + $body[] = new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('output'), + $this->factory->methodCall( + new Node\Expr\New_( + new Node\Name\FullyQualified('ReflectionClass'), + [new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name($definition->getSourceClassName()), 'class'))] + ), + 'newInstanceWithoutConstructor' + ))); + } else { + $constructorArguments = []; + + foreach ($definition->getConstructorArguments() as $i => $propertyDefinition) { + $variable = new Node\Expr\ArrayDimFetch(new Node\Expr\Variable('data'), new Node\Scalar\String_($propertyDefinition->getNormalizedName())); + $targetClasses = $propertyDefinition->getNonPrimitiveTypes(); + $canBeIterable = $propertyDefinition->isCollection(); + + $defaultValue = $propertyDefinition->getConstructorDefaultValue(); + if (\is_object($defaultValue)) { + // public function __construct($foo = new \stdClass()); + // There is no support for parameters to the object. + $defaultValue = new Node\Expr\New_(new Node\Name\FullyQualified($defaultValue::class)); + } else { + $defaultValue = $this->factory->val($defaultValue); + } + + if ([] === $targetClasses && $propertyDefinition->hasConstructorDefaultValue()) { + $constructorArguments[] = new Node\Arg(new Node\Expr\BinaryOp\Coalesce( + $variable, + $defaultValue + )); + continue; + } elseif ([] === $targetClasses) { + $constructorArguments[] = new Node\Arg($variable); + continue; + } + + $needsChildDenormalizer = true; + $tempVariableName = 'argument'.$i; + + if (\count($targetClasses) > 1) { + $variableOutput = $this->generateCodeToDeserializeMultiplePossibleClasses($targetClasses, $canBeIterable, $tempVariableName, $variable, $propertyDefinition->getNormalizedName(), $definition->getNamespaceAndClass()); + } else { + $variableOutput = [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName), + $this->factory->methodCall( + new Node\Expr\Variable('this'), + 'denormalizeChild', + [ + new Node\Arg($variable), + new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($targetClasses[0]), 'class')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name($canBeIterable ? 'true' : 'false'))), + ] + ) + ) + ), + ]; + } + + if ($propertyDefinition->hasConstructorDefaultValue()) { + $variableOutput = [new Node\Stmt\If_(new Node\Expr\BooleanNot( + $this->factory->funcCall('array_key_exists', [ + new Node\Arg(new Node\Scalar\String_($propertyDefinition->getNormalizedName())), + new Node\Arg(new Node\Expr\Variable('data')), + ]) + ), [ + 'stmts' => [ + new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName), + $defaultValue + )), + ], + 'else' => new Node\Stmt\Else_($variableOutput), + ] + )]; + } + + // Add $variableOutput to the end of $body + $body = array_merge($body, $variableOutput); + + $constructorArguments[] = new Node\Arg(new Node\Expr\Variable($tempVariableName)); + } + + $body[] = new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\Variable('output'), + new Node\Expr\New_( + new Node\Name($definition->getSourceClassName()), + $constructorArguments + ), + )); + } + + // Start working with non-constructor properties + $i = 0; + foreach ($definition->getDefinitions() as $propertyDefinition) { + if (!$propertyDefinition->isWriteable() || $propertyDefinition->isConstructorArgument()) { + continue; + } + + $tempVariableName = null; + $variableOutput = []; + + $variable = new Node\Expr\ArrayDimFetch(new Node\Expr\Variable('data'), new Node\Scalar\String_($propertyDefinition->getNormalizedName())); + $targetClasses = $propertyDefinition->getNonPrimitiveTypes(); + + if ([] !== $targetClasses) { + $needsChildDenormalizer = true; + $tempVariableName = 'setter'.$i++; + + if (\count($targetClasses) > 1) { + $variableOutput = $this->generateCodeToDeserializeMultiplePossibleClasses($targetClasses, $propertyDefinition->isCollection(), $tempVariableName, $variable, $propertyDefinition->getNormalizedName(), $definition->getNamespaceAndClass()); + } else { + $variableOutput = [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName), + $this->factory->methodCall( + new Node\Expr\Variable('this'), + 'denormalizeChild', + [ + new Node\Arg($variable), + new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($targetClasses[0]), 'class')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name($propertyDefinition->isCollection() ? 'true' : 'false'))), + ] + ) + ) + ), + ]; + } + } + + $result = null === $tempVariableName ? $variable : new Node\Expr\Variable($tempVariableName); + if (null !== $method = $propertyDefinition->getSetterName()) { + $variableOutput[] = new Node\Stmt\Expression(new Node\Expr\MethodCall( + new Node\Expr\Variable('output'), + $method, + [new Node\Arg($result)] + )); + } else { + $variableOutput[] = new Node\Stmt\Expression(new Node\Expr\Assign( + new Node\Expr\PropertyFetch(new Node\Expr\Variable('output'), $propertyDefinition->getPropertyName()), + $result + )); + } + + $body[] = new Node\Stmt\If_( + $this->factory->funcCall('array_key_exists', [ + new Node\Arg(new Node\Scalar\String_($propertyDefinition->getNormalizedName())), + new Node\Arg(new Node\Expr\Variable('data')), + ]), + ['stmts' => $variableOutput] + ); + } + + $class->addStmt($this->factory->method('denormalize') + ->makePublic() + ->addParam($this->factory->param('data')->setType('mixed')) + ->addParam($this->factory->param('type')->setType('string')) + ->addParam($this->factory->param('format')->setType('?string')->setDefault(null)) + ->addParam($this->factory->param('context')->setType('array')->setDefault([])) + ->setReturnType('mixed') + ->addStmts($body) + ->addStmt(new Node\Stmt\Return_(new Node\Expr\Variable('output'))) + ); + + if ($needsChildDenormalizer) { + $this->generateDenormalizeChildMethod($namespace, $class); + } + } + + private function addNormailizeMethod(ClassDefinition $definition, Namespace_ $namespace, Class_ $class): void + { + $bodyArrayItems = []; + $needsChildNormalizer = false; + foreach ($definition->getDefinitions() as $propertyDefinition) { + if (!$propertyDefinition->isReadable()) { + continue; + } + + if (null !== $method = $propertyDefinition->getGetterName()) { + // $object->$method() + $accessor = $this->factory->methodCall(new Node\Expr\Variable('object'), $method); + } else { + // $object->property + $accessor = $this->factory->propertyFetch(new Node\Expr\Variable('object'), $propertyDefinition->getPropertyName()); + } + + if ($propertyDefinition->hasNoTypeDefinition() || [] !== $propertyDefinition->getNonPrimitiveTypes()) { + $needsChildNormalizer = true; + // $this->normalizeChild($accessor, $format, $context, bool); + $accessor = $this->factory->methodCall(new Node\Expr\Variable('this'), 'normalizeChild', [ + $accessor, + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name($propertyDefinition->isCollection() || $propertyDefinition->hasNoTypeDefinition() ? 'true' : 'false'))), + ]); + } + + $bodyArrayItems[] = new Node\Expr\ArrayItem($accessor, new Node\Scalar\String_($propertyDefinition->getNormalizedName())); + } + + // public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; + $class->addStmt($this->factory->method('normalize') + ->makePublic() + ->addParam($this->factory->param('object')->setType('mixed')) + ->addParam($this->factory->param('format')->setType('string')->setDefault(null)) + ->addParam($this->factory->param('context')->setType('array')->setDefault([])) + ->setReturnType('array|string|int|float|bool|\ArrayObject|null') + ->setDocComment(sprintf('/**'.\PHP_EOL.'* @param %s $object'.\PHP_EOL.'*/', $definition->getSourceClassName())) + ->addStmt(new Node\Stmt\Return_(new Node\Expr\Array_($bodyArrayItems)))); + + if ($needsChildNormalizer) { + $this->generateNormalizeChildMethod($namespace, $class); + } + } + + /** + * When the type-hint has many different classes, then we need to try to denormalize them + * one by one. We are happy when we dont get any exceptions thrown. + * + * @return Node\Stmt[] + */ + private function generateCodeToDeserializeMultiplePossibleClasses(array $targetClasses, bool $canBeIterable, string $tempVariableName, Node\Expr $variable, string $keyName, string $classNs): array + { + $arrayItems = []; + foreach ($targetClasses as $class) { + $arrayItems[] = new Node\Expr\ArrayItem(new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($class), 'class')); + } + + return [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('exceptions'), + new Node\Expr\Array_() + ) + ), + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName.'HasValue'), + new Node\Expr\ConstFetch(new Node\Name('false')) + ) + ), + new Node\Stmt\Foreach_( + new Node\Expr\Array_($arrayItems), + new Node\Expr\Variable('class'), + [ + 'stmts' => [ + new Node\Stmt\TryCatch( + // statements + [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName), + $this->factory->methodCall( + new Node\Expr\Variable('this'), + 'denormalizeChild', + [ + new Node\Arg($variable), + new Node\Arg(new Node\Expr\Variable('class')), + new Node\Arg(new Node\Expr\Variable('format')), + new Node\Arg(new Node\Expr\Variable('context')), + new Node\Arg(new Node\Expr\ConstFetch(new Node\Name($canBeIterable ? 'true' : 'false'))), + ] + ) + ) + ), + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable($tempVariableName.'HasValue'), + new Node\Expr\ConstFetch(new Node\Name('true')) + ) + ), + new Node\Stmt\Break_(), + ], + // Catches + [ + new Node\Stmt\Catch_( + [new Node\Name\FullyQualified(\Throwable::class)], + new Node\Expr\Variable('e'), + [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\ArrayDimFetch(new Node\Expr\Variable('exceptions')), + new Node\Expr\Variable('e') + ) + ), + ] + ), + ], + ), + ], + ] + ), // end foreach + new Node\Stmt\If_( + new Node\Expr\BooleanNot(new Node\Expr\Variable($tempVariableName.'HasValue')), + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Throw_( + new Node\Expr\New_( + new Node\Name('DenormalizingUnionFailedException'), + [ + new Node\Arg(new Node\Scalar\String_('Failed to denormalize key "'.$keyName.'" of class "'.$classNs.'".')), + new Node\Arg(new Node\Expr\Variable('exceptions')), + ] + ) + ), + ), + ], + ] + ), + ]; + } +} diff --git a/src/Symfony/Component/Serializer/Builder/PropertyDefinition.php b/src/Symfony/Component/Serializer/Builder/PropertyDefinition.php new file mode 100644 index 000000000000..9258d8c92a31 --- /dev/null +++ b/src/Symfony/Component/Serializer/Builder/PropertyDefinition.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Builder; + +/** + * All information about a specific property to be able to build a good Normalizer. + * + * @author Tobias Nyholm + * + * @internal + * + * @experimental in 7.1 + */ +class PropertyDefinition +{ + private string $propertyName; + private ?string $normalizedName = null; + private ?string $getterName = null; + private ?string $setterName = null; + private bool $isCollection = false; + private ?int $constructorArgument = null; + private mixed $constructorDefaultValue = null; + private bool $hasConstructorDefaultValue = false; + private bool $isReadable = false; + private bool $isWriteable = false; + + /** + * Ie, other classes. + * + * @var string[] + */ + private array $nonPrimitiveTypes = []; + + /** + * string, int, float, bool, null. + * + * @var string[] + */ + private array $scalarTypes = []; + + public function __construct(string $propertyName) + { + $this->propertyName = $propertyName; + } + + public function getPropertyName(): string + { + return $this->propertyName; + } + + public function isConstructorArgument(): bool + { + return null !== $this->constructorArgument; + } + + public function getConstructorArgumentOrder(): ?int + { + return $this->constructorArgument; + } + + /** + * First argument is 0, next argument is 1 etc.. + */ + public function setConstructorArgumentOrder(int $constructorArgument): void + { + $this->constructorArgument = $constructorArgument; + } + + public function setIsReadable(bool $isReadable): void + { + $this->isReadable = $isReadable; + } + + public function setIsWriteable(bool $isWriteable): void + { + $this->isWriteable = $isWriteable; + } + + public function setIsCollection(bool $isCollection): void + { + $this->isCollection = $isCollection; + } + + public function setNonPrimitiveTypes(array $nonPrimitiveTypes): void + { + $this->nonPrimitiveTypes = $nonPrimitiveTypes; + } + + public function setGetterName(?string $getterName): void + { + $this->getterName = $getterName; + } + + public function setSetterName(?string $setterName): void + { + $this->setterName = $setterName; + } + + public function isReadable(): bool + { + return $this->isReadable; + } + + public function isWriteable(): bool + { + return $this->isWriteable || $this->isConstructorArgument(); + } + + public function getGetterName(): ?string + { + return $this->getterName; + } + + public function getSetterName(): ?string + { + return $this->setterName; + } + + public function isCollection(): bool + { + return $this->isCollection; + } + + public function getNormalizedName(): string + { + return $this->normalizedName ?? $this->propertyName; + } + + /** + * @return string[] + */ + public function getNonPrimitiveTypes(): array + { + return $this->nonPrimitiveTypes; + } + + public function hasNoTypeDefinition(): bool + { + return [] === $this->nonPrimitiveTypes && [] === $this->scalarTypes; + } + + public function isOnlyScalarTypes(): ?bool + { + return [] === $this->nonPrimitiveTypes && [] !== $this->scalarTypes; + } + + public function setScalarTypes(array $scalarTypes): void + { + $this->scalarTypes = $scalarTypes; + } + + public function getConstructorDefaultValue(): mixed + { + return $this->constructorDefaultValue; + } + + public function setConstructorDefaultValue(mixed $constructorDefaultValue): void + { + $this->constructorDefaultValue = $constructorDefaultValue; + $this->hasConstructorDefaultValue = true; + } + + public function hasConstructorDefaultValue(): bool + { + return $this->hasConstructorDefaultValue; + } +} diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index b329cf154233..a2b35fd36a02 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add attribute `Serializable` to automatically build a normalizer from a PHP class + 7.0 --- diff --git a/src/Symfony/Component/Serializer/DependencyInjection/CustomNormalizerHelper.php b/src/Symfony/Component/Serializer/DependencyInjection/CustomNormalizerHelper.php new file mode 100644 index 000000000000..f2dc0cbef720 --- /dev/null +++ b/src/Symfony/Component/Serializer/DependencyInjection/CustomNormalizerHelper.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\DependencyInjection; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Serializer\Attribute\Serializable; +use Symfony\Component\Serializer\Builder\DefinitionExtractor; +use Symfony\Component\Serializer\Builder\NormalizerBuilder; + +/** + * Create custom normalizers and denormalizers. This class is used to glue things + * together with FrameworkBundle. + * + * @author Tobias Nyholm + * + * @internal + */ +class CustomNormalizerHelper +{ + public function __construct( + private NormalizerBuilder $builder, + private DefinitionExtractor $definitionExtractor, + private array $paths, + private string $projectDir, + private ?LoggerInterface $logger = null, + ) { + } + + public function build(string $outputDir): iterable + { + foreach ($this->paths as $prefix => $inputPath) { + $path = $this->projectDir.\DIRECTORY_SEPARATOR.$inputPath; + if (!is_dir($path)) { + $this->logger?->error(sprintf('Path "%s" is not a directory', $path)); + continue; + } + + $finder = new Finder(); + $finder + ->files() + ->in($path) + ->name('/\.php$/'); + + foreach ($finder as $file) { + $classNs = $this->getClassName($prefix, $file); + if (!class_exists($classNs)) { + $this->logger?->warning(sprintf('Failed to guess class name for file "%s"', $file->getRealPath())); + continue; + } + + $reflectionClass = new \ReflectionClass($classNs); + if ([] === $reflectionClass->getAttributes(Serializable::class, \ReflectionAttribute::IS_INSTANCEOF)) { + continue; + } + + $classDefinition = $this->definitionExtractor->getDefinition($classNs); + yield $this->builder->build($classDefinition, $outputDir); + } + } + } + + /** + * @return class-string + */ + private function getClassName(string $prefix, SplFileInfo $file): string + { + $namespace = rtrim(sprintf('%s\\%s', $prefix, $file->getRelativePath()), '\\'); + + return sprintf('%s\\%s', $namespace, $file->getFilenameWithoutExtension()); + } +} diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php index 2a429054b0c7..67d2505d82ea 100644 --- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php +++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; @@ -24,8 +25,10 @@ * Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as * encoders and normalizers to the "serializer" service. * - * @author Javier Lopez + * It also builds all custom Normalizer classes. + * * @author Robin Chalas + * @author Tobias Nyholm */ class SerializerPass implements CompilerPassInterface { @@ -37,6 +40,8 @@ public function process(ContainerBuilder $container): void return; } + $this->buildNormalizers($container); + if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container)) { throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.'); } @@ -71,4 +76,22 @@ public function process(ContainerBuilder $container): void $serializerDefinition->replaceArgument(0, $normalizers); $serializerDefinition->replaceArgument(1, $encoders); } + + public function buildNormalizers(ContainerBuilder $container): void + { + if (!$container->hasDefinition('serializer.custom_normalizer_helper')) { + return; + } + + $directory = $container->getParameter('kernel.build_dir').\DIRECTORY_SEPARATOR.'Symfony'.\DIRECTORY_SEPARATOR.'Serializer'.\DIRECTORY_SEPARATOR.'Normalizer'; + + /** @var CustomNormalizerHelper $builder */ + $builder = $container->get('serializer.custom_normalizer_helper'); + foreach ($builder->build($directory) as $result) { + $definition = new Definition($result->classNs); + $definition->setFile($result->filePath); + $definition->addTag('serializer.normalizer', ['priority' => 110]); + $container->setDefinition('serializer.normalizer.'.$result->className, $definition); + } + } } diff --git a/src/Symfony/Component/Serializer/Exception/DenormalizingUnionFailedException.php b/src/Symfony/Component/Serializer/Exception/DenormalizingUnionFailedException.php new file mode 100644 index 000000000000..bd6b3de2d6bb --- /dev/null +++ b/src/Symfony/Component/Serializer/Exception/DenormalizingUnionFailedException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Exception; + +class DenormalizingUnionFailedException extends \RuntimeException +{ + private array $exceptions; + + public function __construct(string $message, array $exceptions) + { + $this->exceptions = $exceptions; + $first = $exceptions[array_key_first($exceptions)] ?? null; + parent::__construct($message, 0, $first); + } + + /** + * @return \Throwable[] + */ + public function getExceptions(): array + { + return $this->exceptions; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Builder/FixtureHelper.php b/src/Symfony/Component/Serializer/Tests/Builder/FixtureHelper.php new file mode 100644 index 000000000000..2be4c785b62f --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Builder/FixtureHelper.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Builder; + +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorAggregate; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\Serializer\Builder\DefinitionExtractor; +use Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints; +use Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\NoTypeHints; + +class FixtureHelper +{ + public static function getDefinitionExtractor(): DefinitionExtractor + { + $reflectionExtractor = new ReflectionExtractor(); + $constructorArgumentExtractor = new ConstructorArgumentTypeExtractorAggregate([ + $reflectionExtractor, + new PhpDocExtractor(), + ]); + + return new DefinitionExtractor( + propertyInfo: self::getPropertyInfoExtractor(), + propertyReadInfoExtractor: $reflectionExtractor, + propertyWriteInfoExtractor: $reflectionExtractor, + constructorArgumentTypeExtractor: $constructorArgumentExtractor, + ); + } + + public static function getFixturesAndResultFiles(): iterable + { + $rootDir = \dirname(__DIR__).'/Fixtures/CustomNormalizer'; + + return [ + NoTypeHints\PublicProperties::class => $rootDir.'/NoTypeHints/ExpectedNormalizer/PublicProperties.php', + NoTypeHints\ConstructorInjection::class => $rootDir.'/NoTypeHints/ExpectedNormalizer/ConstructorInjection.php', + NoTypeHints\SetterInjection::class => $rootDir.'/NoTypeHints/ExpectedNormalizer/SetterInjection.php', + NoTypeHints\ConstructorAndSetterInjection::class => $rootDir.'/NoTypeHints/ExpectedNormalizer/ConstructorAndSetterInjection.php', + NoTypeHints\InheritanceChild::class => $rootDir.'/NoTypeHints/ExpectedNormalizer/InheritanceChild.php', + + FullTypeHints\PublicProperties::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/PublicProperties.php', + FullTypeHints\ConstructorInjection::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ConstructorInjection.php', + FullTypeHints\SetterInjection::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/SetterInjection.php', + FullTypeHints\InheritanceChild::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/InheritanceChild.php', + FullTypeHints\PrivateConstructor::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/PrivateConstructor.php', + FullTypeHints\ConstructorWithDefaultValue::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ConstructorWithDefaultValue.php', + FullTypeHints\ComplexTypesConstructor::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ComplexTypesConstructor.php', + FullTypeHints\ComplexTypesPublicProperties::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ComplexTypesPublicProperties.php', + FullTypeHints\ComplexTypesSetter::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ComplexTypesSetter.php', + FullTypeHints\ExtraSetter::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/ExtraSetter.php', + FullTypeHints\NonReadableProperty::class => $rootDir.'/FullTypeHints/ExpectedNormalizer/NonReadableProperty.php', + ]; + } + + private static function getPropertyInfoExtractor(): PropertyInfoExtractor + { + // a full list of extractors is shown further below + $phpDocExtractor = new PhpDocExtractor(); + $reflectionExtractor = new ReflectionExtractor(); + + // list of PropertyListExtractorInterface (any iterable) + $listExtractors = [$reflectionExtractor]; + + // list of PropertyTypeExtractorInterface (any iterable) + $typeExtractors = [$phpDocExtractor, $reflectionExtractor]; + + // list of PropertyDescriptionExtractorInterface (any iterable) + $descriptionExtractors = [$phpDocExtractor]; + + // list of PropertyAccessExtractorInterface (any iterable) + $accessExtractors = [$reflectionExtractor]; + + // list of PropertyInitializableExtractorInterface (any iterable) + $propertyInitializableExtractors = [$reflectionExtractor]; + + return new PropertyInfoExtractor( + $listExtractors, + $typeExtractors, + $descriptionExtractors, + $accessExtractors, + $propertyInitializableExtractors + ); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Builder/NormalizerBuilderFixtureTest.php b/src/Symfony/Component/Serializer/Tests/Builder/NormalizerBuilderFixtureTest.php new file mode 100644 index 000000000000..78bde70217b8 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Builder/NormalizerBuilderFixtureTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Builder; + +use PhpParser\ParserFactory; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Builder\DefinitionExtractor; +use Symfony\Component\Serializer\Builder\NormalizerBuilder; + +class NormalizerBuilderFixtureTest extends TestCase +{ + private static NormalizerBuilder $builder; + private static DefinitionExtractor $definitionExtractor; + private static string $outputDir; + private static bool $compareOutput; + + public static function setUpBeforeClass(): void + { + self::$definitionExtractor = FixtureHelper::getDefinitionExtractor(); + self::$outputDir = \dirname(__DIR__).'/_output/SerializerBuilderFixtureTest'; + self::$builder = new NormalizerBuilder(); + + // Only compare on nikic/php-parser: ^5.0 + self::$compareOutput = method_exists(ParserFactory::class, 'createForVersion'); + + parent::setUpBeforeClass(); + } + + /** + * If one does changes to the NormalizerBuilder, this test will probably fail. + * Run `php Tests/Builder/generateUpdatedFixtures.php` to update the fixtures. + * + * This will help reviewers to see the effect of the changes in the NormalizerBuilder. + * + * @dataProvider fixtureClassGenerator + */ + public function testBuildFixtures(string $inputClass, string $expectedOutputFile) + { + $def = self::$definitionExtractor->getDefinition($inputClass); + $result = self::$builder->build($def, self::$outputDir); + $result->loadClass(); + $this->assertTrue(class_exists($result->classNs)); + + if (self::$compareOutput) { + $this->assertFileEquals($expectedOutputFile, $result->filePath); + } + } + + public static function fixtureClassGenerator(): iterable + { + foreach (FixtureHelper::getFixturesAndResultFiles() as $class => $normalizerFile) { + yield $class => [$class, $normalizerFile]; + } + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Builder/generateUpdatedFixtures.php b/src/Symfony/Component/Serializer/Tests/Builder/generateUpdatedFixtures.php new file mode 100644 index 000000000000..bfaffd56f0cb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Builder/generateUpdatedFixtures.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +function includeIfExists(string $file): bool +{ + return file_exists($file) && include $file; +} + +if ( + !includeIfExists(__DIR__.'/../../../../autoload.php') + && !includeIfExists(__DIR__.'/../../vendor/autoload.php') + && !includeIfExists(__DIR__.'/../../../../../../vendor/autoload.php') +) { + fwrite(\STDERR, 'Install dependencies using Composer.'.\PHP_EOL); + exit(1); +} + +use Symfony\Component\Serializer\Builder\NormalizerBuilder; +use Symfony\Component\Serializer\Tests\Builder\FixtureHelper; + +$outputDir = sys_get_temp_dir(); +$definitionExtractor = FixtureHelper::getDefinitionExtractor(); +$builder = new NormalizerBuilder(); + +echo \PHP_EOL; +$i = 0; +foreach (FixtureHelper::getFixturesAndResultFiles() as $class => $outputFile) { + $definition = $definitionExtractor->getDefinition($class); + $result = $builder->build($definition, $outputDir); + $result->loadClass(); + file_put_contents($outputFile, file_get_contents($result->filePath)); + echo '.'; + if (0 === ++$i % 20) { + echo \PHP_EOL; + } +} + +echo \PHP_EOL.\PHP_EOL; +echo 'Done generating fixtures.'; +echo \PHP_EOL; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesConstructor.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesConstructor.php new file mode 100644 index 000000000000..1d8152bc6b55 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesConstructor.php @@ -0,0 +1,82 @@ +simple = $simple; + $this->array = $array; + $this->union = $union; + $this->nested = $nested; + $this->unionArray = $unionArray; + $this->simpleArray = $simpleArray; + } + + public function getSimple(): DummyObject + { + return $this->simple; + } + + + public function getArray(): array + { + return $this->array; + } + + + public function getUnion(): SmartObject|DummyObject + { + return $this->union; + } + + + public function getNested(): DummyObject&SmartObject + { + return $this->nested; + } + + public function getUnionArray(): array + { + return $this->unionArray; + } + + /** + * @return array + */ + public function getSimpleArray(): array + { + return $this->simpleArray; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesPublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesPublicProperties.php new file mode 100644 index 000000000000..bdfa2ead4a93 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ComplexTypesPublicProperties.php @@ -0,0 +1,22 @@ +simple; + } + + + public function getArray(): array + { + return $this->array; + } + + + public function getUnion(): SmartObject|DummyObject + { + return $this->union; + } + + + public function getNested(): DummyObject&SmartObject + { + return $this->nested; + } + + public function getUnionArray(): array + { + return $this->unionArray; + } + + public function setSimple(DummyObject $simple): void + { + $this->simple = $simple; + } + + public function setArray(array $array): void + { + $this->array = $array; + } + + public function setUnion(SmartObject|DummyObject $union): void + { + $this->union = $union; + } + + public function setNested(DummyObject&SmartObject $nested): void + { + $this->nested = $nested; + } + + public function setUnionArray(array $unionArray): void + { + $this->unionArray = $unionArray; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorInjection.php new file mode 100644 index 000000000000..dd4b3679cbde --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorInjection.php @@ -0,0 +1,77 @@ +name = $name; + $this->age = $age; + $this->height = $height; + $this->handsome = $handsome; + $this->nameOfFriends = $nameOfFriends; + $this->picture = $picture; + $this->pet = $pet; + $this->relation = $relation; + } + + public function getName(): string + { + return $this->name; + } + + public function getAge(): int + { + return $this->age; + } + + public function getHeight(): float + { + return $this->height; + } + + public function isHandsome(): bool + { + return $this->handsome; + } + + public function getNameOfFriends(): array + { + return $this->nameOfFriends; + } + + public function getPicture() + { + return $this->picture; + } + + public function getPet(): ?string + { + return $this->pet; + } + + public function getRelation(): DummyObject + { + return $this->relation; + } + + public function getNotSet(): string + { + return $this->notSet; + } + + +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorWithDefaultValue.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorWithDefaultValue.php new file mode 100644 index 000000000000..2ccc836e6c3c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ConstructorWithDefaultValue.php @@ -0,0 +1,25 @@ +foo = $foo; + $this->union = $union; + } + + public function getFoo(): int + { + return $this->foo; + } + + public function getUnion(): SmartObject|DummyObject|null + { + return $this->union; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/DummyObject.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/DummyObject.php new file mode 100644 index 000000000000..3a4d0a1d7c6d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/DummyObject.php @@ -0,0 +1,8 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ComplexTypesConstructor; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ComplexTypesConstructor::class; + } + /** + * @param ComplexTypesConstructor $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['simple' => $this->normalizeChild($object->getSimple(), $format, $context, false), 'simpleArray' => $object->getSimpleArray(), 'array' => $this->normalizeChild($object->getArray(), $format, $context, true), 'union' => $this->normalizeChild($object->getUnion(), $format, $context, false), 'nested' => $this->normalizeChild($object->getNested(), $format, $context, false), 'unionArray' => $this->normalizeChild($object->getUnionArray(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $argument0 = $this->denormalizeChild($data['simple'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $argument2 = $this->denormalizeChild($data['array'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, true); + $exceptions = []; + $argument3HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class] as $class) { + try { + $argument3 = $this->denormalizeChild($data['union'], $class, $format, $context, false); + $argument3HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$argument3HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "union" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesConstructor".', $exceptions); + } + $exceptions = []; + $argument4HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $argument4 = $this->denormalizeChild($data['nested'], $class, $format, $context, false); + $argument4HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$argument4HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "nested" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesConstructor".', $exceptions); + } + $exceptions = []; + $argument5HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $argument5 = $this->denormalizeChild($data['unionArray'], $class, $format, $context, true); + $argument5HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$argument5HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "unionArray" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesConstructor".', $exceptions); + } + $output = new ComplexTypesConstructor($argument0, $data['simpleArray'], $argument2, $argument3, $argument4, $argument5); + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesPublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesPublicProperties.php new file mode 100644 index 000000000000..b2046cef874e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesPublicProperties.php @@ -0,0 +1,127 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ComplexTypesPublicProperties; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ComplexTypesPublicProperties::class; + } + /** + * @param ComplexTypesPublicProperties $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['simple' => $this->normalizeChild($object->simple, $format, $context, false), 'array' => $this->normalizeChild($object->array, $format, $context, true), 'union' => $this->normalizeChild($object->union, $format, $context, false), 'nested' => $this->normalizeChild($object->nested, $format, $context, false), 'unionArray' => $this->normalizeChild($object->unionArray, $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new ComplexTypesPublicProperties(); + if (array_key_exists('simple', $data)) { + $setter0 = $this->denormalizeChild($data['simple'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $output->simple = $setter0; + } + if (array_key_exists('array', $data)) { + $setter1 = $this->denormalizeChild($data['array'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, true); + $output->array = $setter1; + } + if (array_key_exists('union', $data)) { + $exceptions = []; + $setter2HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $setter2 = $this->denormalizeChild($data['union'], $class, $format, $context, false); + $setter2HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter2HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "union" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesPublicProperties".', $exceptions); + } + $output->union = $setter2; + } + if (array_key_exists('nested', $data)) { + $exceptions = []; + $setter3HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $setter3 = $this->denormalizeChild($data['nested'], $class, $format, $context, false); + $setter3HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter3HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "nested" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesPublicProperties".', $exceptions); + } + $output->nested = $setter3; + } + if (array_key_exists('unionArray', $data)) { + $exceptions = []; + $setter4HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $setter4 = $this->denormalizeChild($data['unionArray'], $class, $format, $context, true); + $setter4HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter4HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "unionArray" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesPublicProperties".', $exceptions); + } + $output->unionArray = $setter4; + } + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesSetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesSetter.php new file mode 100644 index 000000000000..a3e2855adead --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ComplexTypesSetter.php @@ -0,0 +1,127 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ComplexTypesSetter; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ComplexTypesSetter::class; + } + /** + * @param ComplexTypesSetter $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['simple' => $this->normalizeChild($object->getSimple(), $format, $context, false), 'array' => $this->normalizeChild($object->getArray(), $format, $context, true), 'union' => $this->normalizeChild($object->getUnion(), $format, $context, false), 'nested' => $this->normalizeChild($object->getNested(), $format, $context, false), 'unionArray' => $this->normalizeChild($object->getUnionArray(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new ComplexTypesSetter(); + if (array_key_exists('simple', $data)) { + $setter0 = $this->denormalizeChild($data['simple'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $output->setSimple($setter0); + } + if (array_key_exists('array', $data)) { + $setter1 = $this->denormalizeChild($data['array'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, true); + $output->setArray($setter1); + } + if (array_key_exists('union', $data)) { + $exceptions = []; + $setter2HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class] as $class) { + try { + $setter2 = $this->denormalizeChild($data['union'], $class, $format, $context, false); + $setter2HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter2HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "union" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesSetter".', $exceptions); + } + $output->setUnion($setter2); + } + if (array_key_exists('nested', $data)) { + $exceptions = []; + $setter3HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $setter3 = $this->denormalizeChild($data['nested'], $class, $format, $context, false); + $setter3HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter3HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "nested" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesSetter".', $exceptions); + } + $output->setNested($setter3); + } + if (array_key_exists('unionArray', $data)) { + $exceptions = []; + $setter4HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class] as $class) { + try { + $setter4 = $this->denormalizeChild($data['unionArray'], $class, $format, $context, true); + $setter4HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$setter4HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "unionArray" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ComplexTypesSetter".', $exceptions); + } + $output->setUnionArray($setter4); + } + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorInjection.php new file mode 100644 index 000000000000..33e700d53196 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorInjection.php @@ -0,0 +1,69 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ConstructorInjection; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ConstructorInjection::class; + } + /** + * @param ConstructorInjection $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $object->getName(), 'age' => $object->getAge(), 'height' => $object->getHeight(), 'handsome' => $object->isHandsome(), 'nameOfFriends' => $this->normalizeChild($object->getNameOfFriends(), $format, $context, true), 'picture' => $this->normalizeChild($object->getPicture(), $format, $context, true), 'pet' => $object->getPet(), 'relation' => $this->normalizeChild($object->getRelation(), $format, $context, false), 'notSet' => $object->getNotSet()]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $argument7 = $this->denormalizeChild($data['relation'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $output = new ConstructorInjection($data['name'], $data['age'], $data['height'], $data['handsome'], $data['nameOfFriends'], $data['picture'], $data['pet'], $argument7); + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorWithDefaultValue.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorWithDefaultValue.php new file mode 100644 index 000000000000..7dfdac5f0949 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ConstructorWithDefaultValue.php @@ -0,0 +1,86 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ConstructorWithDefaultValue; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ConstructorWithDefaultValue::class; + } + /** + * @param ConstructorWithDefaultValue $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['foo' => $object->getFoo(), 'union' => $this->normalizeChild($object->getUnion(), $format, $context, false)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + if (!array_key_exists('union', $data)) { + $argument1 = null; + } else { + $exceptions = []; + $argument1HasValue = false; + foreach ([\Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject::class, \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class] as $class) { + try { + $argument1 = $this->denormalizeChild($data['union'], $class, $format, $context, false); + $argument1HasValue = true; + break; + } catch (\Throwable $e) { + $exceptions[] = $e; + } + } + if (!$argument1HasValue) { + throw new DenormalizingUnionFailedException('Failed to denormalize key "union" of class "Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\ConstructorWithDefaultValue".', $exceptions); + } + } + $output = new ConstructorWithDefaultValue($data['foo'] ?? 4711, $argument1, $data['x'] ?? new \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\SmartObject()); + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ExtraSetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ExtraSetter.php new file mode 100644 index 000000000000..e7018f526b21 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/ExtraSetter.php @@ -0,0 +1,38 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ExtraSetter; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ExtraSetter::class; + } + /** + * @param ExtraSetter $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $object->getName(), 'age' => $object->getAge()]; + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new ExtraSetter($data['name']); + if (array_key_exists('age', $data)) { + $output->setAge($data['age']); + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/InheritanceChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/InheritanceChild.php new file mode 100644 index 000000000000..6f6db40dec05 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/InheritanceChild.php @@ -0,0 +1,56 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof InheritanceChild; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === InheritanceChild::class; + } + /** + * @param InheritanceChild $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['childCute' => $object->getChildCute(), 'cute' => $object->isCute(), 'childName' => $object->childName, 'name' => $object->name, 'childAge' => $object->getChildAge(), 'childHeight' => $object->getChildHeight(), 'age' => $object->getAge(), 'height' => $object->getHeight(), 'handsome' => $object->isHandsome()]; + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new InheritanceChild($data['childCute'], $data['cute']); + if (array_key_exists('childName', $data)) { + $output->childName = $data['childName']; + } + if (array_key_exists('name', $data)) { + $output->name = $data['name']; + } + if (array_key_exists('childAge', $data)) { + $output->setChildAge($data['childAge']); + } + if (array_key_exists('childHeight', $data)) { + $output->setChildHeight($data['childHeight']); + } + if (array_key_exists('age', $data)) { + $output->setAge($data['age']); + } + if (array_key_exists('height', $data)) { + $output->setHeight($data['height']); + } + if (array_key_exists('handsome', $data)) { + $output->setHandsome($data['handsome']); + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/NonReadableProperty.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/NonReadableProperty.php new file mode 100644 index 000000000000..e2a8da8759c1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/NonReadableProperty.php @@ -0,0 +1,51 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof NonReadableProperty; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === NonReadableProperty::class; + } + /** + * @param NonReadableProperty $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $object->getName(), 'funnyName' => $this->normalizeChild($object->getFunnyName(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new NonReadableProperty($data['name']); + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PrivateConstructor.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PrivateConstructor.php new file mode 100644 index 000000000000..3f37ee254644 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PrivateConstructor.php @@ -0,0 +1,38 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof PrivateConstructor; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === PrivateConstructor::class; + } + /** + * @param PrivateConstructor $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['foo' => $object->foo]; + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = (new \ReflectionClass(PrivateConstructor::class))->newInstanceWithoutConstructor(); + if (array_key_exists('foo', $data)) { + $output->foo = $data['foo']; + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PublicProperties.php new file mode 100644 index 000000000000..c7d69b2229bf --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/PublicProperties.php @@ -0,0 +1,93 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof PublicProperties; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === PublicProperties::class; + } + /** + * @param PublicProperties $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $object->name, 'age' => $object->age, 'height' => $object->height, 'handsome' => $object->handsome, 'nameOfFriends' => $this->normalizeChild($object->nameOfFriends, $format, $context, true), 'picture' => $this->normalizeChild($object->picture, $format, $context, true), 'pet' => $object->pet, 'relation' => $this->normalizeChild($object->relation, $format, $context, false)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new PublicProperties(); + if (array_key_exists('name', $data)) { + $output->name = $data['name']; + } + if (array_key_exists('age', $data)) { + $output->age = $data['age']; + } + if (array_key_exists('height', $data)) { + $output->height = $data['height']; + } + if (array_key_exists('handsome', $data)) { + $output->handsome = $data['handsome']; + } + if (array_key_exists('nameOfFriends', $data)) { + $output->nameOfFriends = $data['nameOfFriends']; + } + if (array_key_exists('picture', $data)) { + $output->picture = $data['picture']; + } + if (array_key_exists('pet', $data)) { + $output->pet = $data['pet']; + } + if (array_key_exists('relation', $data)) { + $setter0 = $this->denormalizeChild($data['relation'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $output->relation = $setter0; + } + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/SetterInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/SetterInjection.php new file mode 100644 index 000000000000..f33937186f73 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExpectedNormalizer/SetterInjection.php @@ -0,0 +1,93 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof SetterInjection; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === SetterInjection::class; + } + /** + * @param SetterInjection $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $object->getName(), 'age' => $object->getAge(), 'height' => $object->getHeight(), 'handsome' => $object->isHandsome(), 'nameOfFriends' => $this->normalizeChild($object->getNameOfFriends(), $format, $context, true), 'picture' => $this->normalizeChild($object->getPicture(), $format, $context, true), 'pet' => $object->getPet(), 'relation' => $this->normalizeChild($object->getRelation(), $format, $context, false), 'notSet' => $object->getNotSet()]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new SetterInjection(); + if (array_key_exists('name', $data)) { + $output->setName($data['name']); + } + if (array_key_exists('age', $data)) { + $output->setAge($data['age']); + } + if (array_key_exists('height', $data)) { + $output->setHeight($data['height']); + } + if (array_key_exists('handsome', $data)) { + $output->setHandsome($data['handsome']); + } + if (array_key_exists('nameOfFriends', $data)) { + $output->setNameOfFriends($data['nameOfFriends']); + } + if (array_key_exists('picture', $data)) { + $output->setPicture($data['picture']); + } + if (array_key_exists('pet', $data)) { + $output->setPet($data['pet']); + } + if (array_key_exists('relation', $data)) { + $setter0 = $this->denormalizeChild($data['relation'], \Symfony\Component\Serializer\Tests\Fixtures\CustomNormalizer\FullTypeHints\DummyObject::class, $format, $context, false); + $output->setRelation($setter0); + } + return $output; + } + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + $this->denormalizer = $denormalizer; + } + private function denormalizeChild(mixed $data, string $type, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($data) || null === $data) { + return $data; + } + if ($canBeIterable && is_iterable($data)) { + return array_map(fn($item) => $this->denormalizeChild($item, $type, $format, $context, true), $data); + } + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExtraSetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExtraSetter.php new file mode 100644 index 000000000000..fc6b1bfc2150 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/ExtraSetter.php @@ -0,0 +1,35 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getAge(): int + { + return $this->age; + } + + public function setAge(int $age): void + { + $this->age = $age; + } + +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceChild.php new file mode 100644 index 000000000000..5e8777f8b92c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceChild.php @@ -0,0 +1,52 @@ +childCute = $childCute; + parent::__construct($cute); + } + + public function getChildCute(): bool + { + return $this->childCute; + } + + public function getChildAge(): int + { + return $this->childAge; + } + + public function setChildAge(int $childAge): void + { + $this->childAge = $childAge; + } + + public function getChildHeight(): float + { + return $this->childHeight; + } + + public function setChildHeight(float $childHeight): void + { + $this->childHeight = $childHeight; + } + + public function getAge(): int + { + return $this->age; + } + + public function setAge(int $age): void + { + $this->age = $age; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceParent.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceParent.php new file mode 100644 index 000000000000..454083c69bda --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/InheritanceParent.php @@ -0,0 +1,42 @@ +cute = $cute; + } + + public function isCute(): bool + { + return $this->cute; + } + + public function getHeight(): float + { + return $this->height; + } + + public function setHeight(float $height): void + { + $this->height = $height; + } + + public function isHandsome(): bool + { + return $this->handsome; + } + + public function setHandsome(bool $handsome): void + { + $this->handsome = $handsome; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/NonReadableProperty.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/NonReadableProperty.php new file mode 100644 index 000000000000..13e62cefa87d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/NonReadableProperty.php @@ -0,0 +1,25 @@ +name = $name; + $this->count = strlen($name); + } + + public function getName(): string + { + return $this->name; + } + + public function getFunnyName() + { + return $this->name.'_'.$this->count; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PrivateConstructor.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PrivateConstructor.php new file mode 100644 index 000000000000..1d51bf646b5d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PrivateConstructor.php @@ -0,0 +1,20 @@ +foo = $foo; + return $model; + } + +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PublicProperties.php new file mode 100644 index 000000000000..dfbf68ca80b1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/PublicProperties.php @@ -0,0 +1,15 @@ +name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getAge(): int + { + return $this->age; + } + + public function setAge(int $age): void + { + $this->age = $age; + } + + public function getHeight(): float + { + return $this->height; + } + + public function setHeight(float $height): void + { + $this->height = $height; + } + + public function isHandsome(): bool + { + return $this->handsome; + } + + public function setHandsome(bool $handsome): void + { + $this->handsome = $handsome; + } + + public function getNameOfFriends(): array + { + return $this->nameOfFriends; + } + + public function setNameOfFriends(array $nameOfFriends): void + { + $this->nameOfFriends = $nameOfFriends; + } + + public function getPicture() + { + return $this->picture; + } + + public function setPicture($picture): void + { + $this->picture = $picture; + } + + public function getPet(): ?string + { + return $this->pet; + } + + public function setPet(?string $pet): void + { + $this->pet = $pet; + } + + public function getRelation(): DummyObject + { + return $this->relation; + } + + public function setRelation(DummyObject $relation): void + { + $this->relation = $relation; + } + + public function getNotSet(): string + { + return $this->notSet; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/SmartObject.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/SmartObject.php new file mode 100644 index 000000000000..0b4dd4e26b17 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/FullTypeHints/SmartObject.php @@ -0,0 +1,8 @@ +name = $name; + $this->age = $age; + $this->picture = $picture; + $this->pet = $pet; + $this->relation = $relation; + } + + public function setHeight($height): void + { + $this->height = $height; + } + + public function setHandsome($handsome): void + { + $this->handsome = $handsome; + } + + public function setNameOfFriends($nameOfFriends): void + { + $this->nameOfFriends = $nameOfFriends; + } + + public function getName() + { + return $this->name; + } + + public function getAge() + { + return $this->age; + } + + public function getHeight() + { + return $this->height; + } + + public function getHandsome() + { + return $this->handsome; + } + + public function getNameOfFriends() + { + return $this->nameOfFriends; + } + + public function getPicture() + { + return $this->picture; + } + + public function getPet() + { + return $this->pet; + } + + public function getRelation() + { + return $this->relation; + } + + public function getNotSet() + { + return $this->notSet; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ConstructorInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ConstructorInjection.php new file mode 100644 index 000000000000..3f4ca262f7ec --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ConstructorInjection.php @@ -0,0 +1,73 @@ +name = $name; + $this->age = $age; + $this->height = $height; + $this->handsome = $handsome; + $this->nameOfFriends = $nameOfFriends; + $this->picture = $picture; + $this->pet = $pet; + $this->relation = $relation; + } + + public function getName() + { + return $this->name; + } + + public function getAge() + { + return $this->age; + } + + public function getHeight() + { + return $this->height; + } + + public function getHandsome() + { + return $this->handsome; + } + + public function getNameOfFriends() + { + return $this->nameOfFriends; + } + + public function getPicture() + { + return $this->picture; + } + + public function getPet() + { + return $this->pet; + } + + public function getRelation() + { + return $this->relation; + } + + public function getNotSet() + { + return $this->notSet; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorAndSetterInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorAndSetterInjection.php new file mode 100644 index 000000000000..db3c3d7557cb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorAndSetterInjection.php @@ -0,0 +1,60 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ConstructorAndSetterInjection; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ConstructorAndSetterInjection::class; + } + /** + * @param ConstructorAndSetterInjection $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $this->normalizeChild($object->getName(), $format, $context, true), 'age' => $this->normalizeChild($object->getAge(), $format, $context, true), 'picture' => $this->normalizeChild($object->getPicture(), $format, $context, true), 'pet' => $this->normalizeChild($object->getPet(), $format, $context, true), 'relation' => $this->normalizeChild($object->getRelation(), $format, $context, true), 'height' => $this->normalizeChild($object->getHeight(), $format, $context, true), 'handsome' => $this->normalizeChild($object->getHandsome(), $format, $context, true), 'nameOfFriends' => $this->normalizeChild($object->getNameOfFriends(), $format, $context, true), 'notSet' => $this->normalizeChild($object->getNotSet(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new ConstructorAndSetterInjection($data['name'], $data['age'], $data['picture'], $data['pet'], $data['relation']); + if (array_key_exists('height', $data)) { + $output->setHeight($data['height']); + } + if (array_key_exists('handsome', $data)) { + $output->setHandsome($data['handsome']); + } + if (array_key_exists('nameOfFriends', $data)) { + $output->setNameOfFriends($data['nameOfFriends']); + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorInjection.php new file mode 100644 index 000000000000..47ad282494da --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/ConstructorInjection.php @@ -0,0 +1,51 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof ConstructorInjection; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === ConstructorInjection::class; + } + /** + * @param ConstructorInjection $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $this->normalizeChild($object->getName(), $format, $context, true), 'age' => $this->normalizeChild($object->getAge(), $format, $context, true), 'height' => $this->normalizeChild($object->getHeight(), $format, $context, true), 'handsome' => $this->normalizeChild($object->getHandsome(), $format, $context, true), 'nameOfFriends' => $this->normalizeChild($object->getNameOfFriends(), $format, $context, true), 'picture' => $this->normalizeChild($object->getPicture(), $format, $context, true), 'pet' => $this->normalizeChild($object->getPet(), $format, $context, true), 'relation' => $this->normalizeChild($object->getRelation(), $format, $context, true), 'notSet' => $this->normalizeChild($object->getNotSet(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new ConstructorInjection($data['name'], $data['age'], $data['height'], $data['handsome'], $data['nameOfFriends'], $data['picture'], $data['pet'], $data['relation']); + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/InheritanceChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/InheritanceChild.php new file mode 100644 index 000000000000..9f092cd53de5 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/InheritanceChild.php @@ -0,0 +1,72 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof InheritanceChild; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === InheritanceChild::class; + } + /** + * @param InheritanceChild $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['childCute' => $this->normalizeChild($object->getChildCute(), $format, $context, true), 'cute' => $this->normalizeChild($object->getCute(), $format, $context, true), 'childName' => $this->normalizeChild($object->childName, $format, $context, true), 'name' => $this->normalizeChild($object->name, $format, $context, true), 'childAge' => $this->normalizeChild($object->getChildAge(), $format, $context, true), 'childHeight' => $this->normalizeChild($object->getChildHeight(), $format, $context, true), 'age' => $this->normalizeChild($object->getAge(), $format, $context, true), 'height' => $this->normalizeChild($object->getHeight(), $format, $context, true), 'handsome' => $this->normalizeChild($object->getHandsome(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new InheritanceChild($data['childCute'], $data['cute']); + if (array_key_exists('childName', $data)) { + $output->childName = $data['childName']; + } + if (array_key_exists('name', $data)) { + $output->name = $data['name']; + } + if (array_key_exists('childAge', $data)) { + $output->setChildAge($data['childAge']); + } + if (array_key_exists('childHeight', $data)) { + $output->setChildHeight($data['childHeight']); + } + if (array_key_exists('age', $data)) { + $output->setAge($data['age']); + } + if (array_key_exists('height', $data)) { + $output->setHeight($data['height']); + } + if (array_key_exists('handsome', $data)) { + $output->setHandsome($data['handsome']); + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/PublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/PublicProperties.php new file mode 100644 index 000000000000..71b298280e1e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/PublicProperties.php @@ -0,0 +1,78 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof PublicProperties; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === PublicProperties::class; + } + /** + * @param PublicProperties $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $this->normalizeChild($object->name, $format, $context, true), 'age' => $this->normalizeChild($object->age, $format, $context, true), 'height' => $this->normalizeChild($object->height, $format, $context, true), 'handsome' => $this->normalizeChild($object->handsome, $format, $context, true), 'nameOfFriends' => $this->normalizeChild($object->nameOfFriends, $format, $context, true), 'picture' => $this->normalizeChild($object->picture, $format, $context, true), 'pet' => $this->normalizeChild($object->pet, $format, $context, true), 'relation' => $this->normalizeChild($object->relation, $format, $context, true), 'notSet' => $this->normalizeChild($object->notSet, $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new PublicProperties(); + if (array_key_exists('name', $data)) { + $output->name = $data['name']; + } + if (array_key_exists('age', $data)) { + $output->age = $data['age']; + } + if (array_key_exists('height', $data)) { + $output->height = $data['height']; + } + if (array_key_exists('handsome', $data)) { + $output->handsome = $data['handsome']; + } + if (array_key_exists('nameOfFriends', $data)) { + $output->nameOfFriends = $data['nameOfFriends']; + } + if (array_key_exists('picture', $data)) { + $output->picture = $data['picture']; + } + if (array_key_exists('pet', $data)) { + $output->pet = $data['pet']; + } + if (array_key_exists('relation', $data)) { + $output->relation = $data['relation']; + } + if (array_key_exists('notSet', $data)) { + $output->notSet = $data['notSet']; + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/SetterInjection.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/SetterInjection.php new file mode 100644 index 000000000000..6b74afcd9d6d --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/ExpectedNormalizer/SetterInjection.php @@ -0,0 +1,75 @@ + true]; + } + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool + { + return $data instanceof SetterInjection; + } + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === SetterInjection::class; + } + /** + * @param SetterInjection $object + */ + public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + return ['name' => $this->normalizeChild($object->getName(), $format, $context, true), 'age' => $this->normalizeChild($object->getAge(), $format, $context, true), 'height' => $this->normalizeChild($object->getHeight(), $format, $context, true), 'handsome' => $object->isHandsome(), 'nameOfFriends' => $this->normalizeChild($object->getNameOfFriends(), $format, $context, true), 'picture' => $this->normalizeChild($object->getPicture(), $format, $context, true), 'pet' => $this->normalizeChild($object->getPet(), $format, $context, true), 'relation' => $this->normalizeChild($object->getRelation(), $format, $context, true), 'notSet' => $this->normalizeChild($object->getNotSet(), $format, $context, true)]; + } + public function setNormalizer(NormalizerInterface $normalizer): void + { + $this->normalizer = $normalizer; + } + private function normalizeChild(mixed $object, ?string $format, array $context, bool $canBeIterable): mixed + { + if (is_scalar($object) || null === $object) { + return $object; + } + if ($canBeIterable && is_iterable($object)) { + return array_map(fn($item) => $this->normalizeChild($item, $format, $context, true), $object); + } + return $this->normalizer->normalize($object, $format, $context); + } + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + $data = (array) $data; + $output = new SetterInjection(); + if (array_key_exists('name', $data)) { + $output->setName($data['name']); + } + if (array_key_exists('age', $data)) { + $output->setAge($data['age']); + } + if (array_key_exists('height', $data)) { + $output->setHeight($data['height']); + } + if (array_key_exists('handsome', $data)) { + $output->setHandsome($data['handsome']); + } + if (array_key_exists('nameOfFriends', $data)) { + $output->setNameOfFriends($data['nameOfFriends']); + } + if (array_key_exists('picture', $data)) { + $output->setPicture($data['picture']); + } + if (array_key_exists('pet', $data)) { + $output->setPet($data['pet']); + } + if (array_key_exists('relation', $data)) { + $output->setRelation($data['relation']); + } + return $output; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceChild.php new file mode 100644 index 000000000000..64b585bd871c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceChild.php @@ -0,0 +1,52 @@ +childCute = $childCute; + parent::__construct($cute); + } + + public function getChildCute() + { + return $this->childCute; + } + + public function getChildAge() + { + return $this->childAge; + } + + public function setChildAge($childAge): void + { + $this->childAge = $childAge; + } + + public function getChildHeight() + { + return $this->childHeight; + } + + public function setChildHeight($childHeight): void + { + $this->childHeight = $childHeight; + } + + public function getAge() + { + return $this->age; + } + + public function setAge($age): void + { + $this->age = $age; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceParent.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceParent.php new file mode 100644 index 000000000000..1cde7e76a3f2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/InheritanceParent.php @@ -0,0 +1,45 @@ +cute = $cute; + } + + public function getCute() + { + return $this->cute; + } + + public function getHeight() + { + return $this->height; + } + + public function setHeight($height): void + { + $this->height = $height; + } + + public function getHandsome() + { + return $this->handsome; + } + + public function setHandsome($handsome): void + { + $this->handsome = $handsome; + } + + + +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/PublicProperties.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/PublicProperties.php new file mode 100644 index 000000000000..9cc64412792b --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CustomNormalizer/NoTypeHints/PublicProperties.php @@ -0,0 +1,17 @@ +name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getAge() + { + return $this->age; + } + + public function setAge($age) + { + $this->age = $age; + } + + public function getHeight() + { + return $this->height; + } + + public function setHeight($height) + { + $this->height = $height; + } + + public function isHandsome() + { + return $this->handsome; + } + + public function setHandsome($handsome) + { + $this->handsome = $handsome; + } + + public function getNameOfFriends() + { + return $this->nameOfFriends; + } + + public function setNameOfFriends($nameOfFriends) + { + $this->nameOfFriends = $nameOfFriends; + } + + public function getPicture() + { + return $this->picture; + } + + public function setPicture($picture) + { + $this->picture = $picture; + } + + public function getPet() + { + return $this->pet; + } + + public function setPet($pet) + { + $this->pet = $pet; + } + + public function getRelation() + { + return $this->relation; + } + + public function setRelation($relation) + { + $this->relation = $relation; + } + + public function getNotSet() + { + return $this->notSet; + } + + + + +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AutoNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AutoNormalizerTest.php new file mode 100644 index 000000000000..b234de6e71f1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AutoNormalizerTest.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer; + +require_once __DIR__.'/ObjectNormalizerTest.php'; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Builder\DefinitionExtractor; +use Symfony\Component\Serializer\Builder\NormalizerBuilder; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\Tests\Builder\FixtureHelper; +use Symfony\Component\Serializer\Tests\Fixtures\DummyPrivatePropertyWithoutGetter; +use Symfony\Component\Serializer\Tests\Fixtures\Sibling; +use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; +use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummy; + +/** + * @author Kévin Dunglas + */ +class AutoNormalizerTest extends TestCase +{ + private static NormalizerBuilder $builder; + private static DefinitionExtractor $definitionExtractor; + private static string $outputDir; + + public static function setUpBeforeClass(): void + { + self::$definitionExtractor = FixtureHelper::getDefinitionExtractor(); + self::$outputDir = \dirname(__DIR__).'/_output/SerializerBuilderFixtureTest'; + self::$builder = new NormalizerBuilder(); + + parent::setUpBeforeClass(); + } + + private function getSerializer(string ...$inputClasses): Serializer + { + $normalizers = []; + foreach($inputClasses as $inputClass) { + $def = self::$definitionExtractor->getDefinition($inputClass); + $result = self::$builder->build($def, self::$outputDir); + $result->loadClass(); + + $normalizers[] = new $result->classNs(); + } + + return new Serializer($normalizers); + } + + public function testNormalizeObjectWithPrivatePropertyWithoutGetter() + { + $serializer = $this->getSerializer(DummyPrivatePropertyWithoutGetter::class); + $obj = new DummyPrivatePropertyWithoutGetter(); + $this->assertEquals( + ['bar' => 'bar'], + $serializer->normalize($obj, 'any') + ); + } + + public function testDenormalize() + { + $serializer = $this->getSerializer(ObjectDummy::class); + $obj = $serializer->denormalize( + ['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'], + ObjectDummy::class, + 'any' + ); + $this->assertEquals('foo', $obj->getFoo()); + $this->assertEquals('bar', $obj->bar); + $this->assertTrue($obj->isBaz()); + } + + public function testDenormalizeWithObject() + { + $serializer = $this->getSerializer(ObjectDummy::class); + $data = new \stdClass(); + $data->foo = 'foo'; + $data->bar = 'bar'; + $data->fooBar = 'foobar'; + $obj = $serializer->denormalize($data, ObjectDummy::class, 'any'); + $this->assertEquals('foo', $obj->getFoo()); + $this->assertEquals('bar', $obj->bar); + } + + public function testDenormalizeNull() + { + $serializer = $this->getSerializer(ObjectDummy::class); + $this->assertEquals(new ObjectDummy(), $serializer->denormalize(null, ObjectDummy::class)); + } + + public function testConstructorDenormalize() + { + $serializer = $this->getSerializer(ObjectConstructorDummy::class); + $obj = $serializer->denormalize( + ['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'], + ObjectConstructorDummy::class, 'any'); + $this->assertEquals('foo', $obj->getFoo()); + $this->assertEquals('bar', $obj->bar); + $this->assertTrue($obj->isBaz()); + } + + public function testConstructorDenormalizeWithNullArgument() + { + $serializer = $this->getSerializer(ObjectConstructorDummy::class); + $obj = $serializer->denormalize( + ['foo' => 'foo', 'bar' => null, 'baz' => true], + ObjectConstructorDummy::class, 'any'); + $this->assertEquals('foo', $obj->getFoo()); + $this->assertNull($obj->bar); + $this->assertTrue($obj->isBaz()); + } + + public function testConstructorDenormalizeWithMissingOptionalArgument() + { + $serializer = $this->getSerializer(ObjectConstructorOptionalArgsDummy::class); + $obj = $serializer->denormalize( + ['foo' => 'test', 'baz' => [1, 2, 3]], + ObjectConstructorOptionalArgsDummy::class, 'any'); + $this->assertEquals('test', $obj->getFoo()); + $this->assertEquals([], $obj->bar); + $this->assertEquals([1, 2, 3], $obj->getBaz()); + } + + public function testConstructorDenormalizeWithOptionalDefaultArgument() + { + $serializer = $this->getSerializer(ObjectConstructorArgsWithDefaultValueDummy::class); + $obj = $serializer->denormalize( + ['bar' => 'test'], + ObjectConstructorArgsWithDefaultValueDummy::class, 'any'); + $this->assertEquals([], $obj->getFoo()); + $this->assertEquals('test', $obj->getBar()); + } + + public function testConstructorWithObjectDenormalize() + { + $serializer = $this->getSerializer(ObjectConstructorDummy::class); + $data = new \stdClass(); + $data->foo = 'foo'; + $data->bar = 'bar'; + $data->baz = true; + $data->fooBar = 'foobar'; + $obj = $serializer->denormalize($data, ObjectConstructorDummy::class, 'any'); + $this->assertEquals('foo', $obj->getFoo()); + $this->assertEquals('bar', $obj->bar); + } + + public function testConstructorWithObjectTypeHintDenormalize() + { + $data = [ + 'id' => 10, + 'inner' => [ + 'foo' => 'oof', + 'bar' => 'rab', + ], + ]; + + + $serializer = $this->getSerializer(DummyWithConstructorObject::class, ObjectInner::class); + $obj = $serializer->denormalize($data, DummyWithConstructorObject::class); + $this->assertInstanceOf(DummyWithConstructorObject::class, $obj); + $this->assertEquals(10, $obj->getId()); + $this->assertInstanceOf(ObjectInner::class, $obj->getInner()); + $this->assertEquals('oof', $obj->getInner()->foo); + $this->assertEquals('rab', $obj->getInner()->bar); + } + + public function testConstructorWithUnconstructableNullableObjectTypeHintDenormalize() + { + $data = [ + 'id' => 10, + 'inner' => null, + ]; + + $serializer = $this->getSerializer(DummyWithNullableConstructorObject::class); + $obj = $serializer->denormalize($data, DummyWithNullableConstructorObject::class); + $this->assertInstanceOf(DummyWithNullableConstructorObject::class, $obj); + $this->assertEquals(10, $obj->getId()); + $this->assertNull($obj->getInner()); + } + + public function testConstructorWithUnknownObjectTypeHintDenormalize() + { + $data = [ + 'id' => 10, + 'unknown' => [ + 'foo' => 'oof', + 'bar' => 'rab', + ], + ]; + + $serializer = $this->getSerializer(DummyWithConstructorInexistingObject::class); + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Could not denormalize object of type "Symfony\Component\Serializer\Tests\Normalizer\Unknown", no supporting normalizer found.'); + + $serializer->denormalize($data, DummyWithConstructorInexistingObject::class); + } + + public function testSiblingReference() + { + $serializer = $this->getSerializer(SiblingHolder::class, Sibling::class); + $siblingHolder = new SiblingHolder(); + + $expected = [ + 'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'], + 'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'], + 'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'], + ]; + $this->assertEquals($expected, $serializer->normalize($siblingHolder)); + } + + public function testDenormalizeNonExistingAttribute() + { + $serializer = $this->getSerializer(ObjectDummy::class); + $this->assertEquals( + new ObjectDummy(), + $serializer->denormalize(['non_existing' => true], ObjectDummy::class) + ); + } + + public function testNormalizeUpperCaseAttributes() + { + $serializer = $this->getSerializer(ObjectWithUpperCaseAttributeNames::class); + $this->assertEquals(['Foo' => 'Foo', 'Bar' => 'BarBar'], $serializer->normalize(new ObjectWithUpperCaseAttributeNames())); + } + + public function testDefaultObjectClassResolver() + { + $serializer = $this->getSerializer(ObjectDummy::class); + + $obj = new ObjectDummy(); + $obj->setFoo('foo'); + $obj->bar = 'bar'; + $obj->setBaz(true); + $obj->setCamelCase('camelcase'); + $obj->unwantedProperty = 'notwanted'; + $obj->setGo(false); + + $this->assertEquals( + [ + 'foo' => 'foo', + 'bar' => 'bar', + 'baz' => true, + 'fooBar' => 'foobar', + 'camelCase' => 'camelcase', + 'object' => null, + 'go' => false, + ], + $serializer->normalize($obj, 'any') + ); + } +} diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 627bfccaf306..6b1e4a071972 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -21,6 +21,7 @@ }, "require-dev": { "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "nikic/php-parser": "^4.16|^5.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", @@ -34,7 +35,7 @@ "symfony/messenger": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", + "symfony/property-info": "^7.1", "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", @@ -47,7 +48,7 @@ "phpdocumentor/type-resolver": "<1.4.0", "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", - "symfony/property-info": "<6.4", + "symfony/property-info": "<7.1", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4"