From b10f1b6893035e1e56c92f26a0afc2e3ba019242 Mon Sep 17 00:00:00 2001 From: Rico Sonntag Date: Thu, 13 Nov 2025 07:39:07 +0100 Subject: [PATCH] Ensure JsonMapper defaults avoid nullable config --- src/JsonMapper.php | 21 +++++++------------ src/JsonMapper/Attribute/ReplaceProperty.php | 6 +++--- .../CollectionDocBlockTypeResolver.php | 12 +++-------- src/JsonMapper/Context/MappingContext.php | 20 +++++++++++------- src/JsonMapper/Context/MappingError.php | 8 +++---- src/JsonMapper/Report/MappingReport.php | 4 ++-- src/JsonMapper/Report/MappingResult.php | 6 +++--- src/JsonMapper/Type/TypeResolver.php | 12 ++++------- .../BuiltinValueConversionStrategy.php | 3 +-- .../DateTimeValueConversionStrategy.php | 3 ++- .../Strategy/EnumValueConversionStrategy.php | 3 ++- .../ObjectValueConversionStrategy.php | 14 ++++++------- tests/TestCase.php | 2 +- 13 files changed, 52 insertions(+), 62 deletions(-) diff --git a/src/JsonMapper.php b/src/JsonMapper.php index f2b08aa..543f02a 100644 --- a/src/JsonMapper.php +++ b/src/JsonMapper.php @@ -105,8 +105,6 @@ class JsonMapper private CustomTypeRegistry $customTypeRegistry; - private JsonMapperConfig $config; - /** * @param array $classMap * @param CacheItemPoolInterface|null $typeCache @@ -119,9 +117,8 @@ public function __construct( private readonly ?PropertyNameConverterInterface $nameConverter = null, array $classMap = [], ?CacheItemPoolInterface $typeCache = null, - ?JsonMapperConfig $config = null, + private JsonMapperConfig $config = new JsonMapperConfig(), ) { - $this->config = $config ?? new JsonMapperConfig(); $this->typeResolver = new TypeResolver($extractor, $typeCache); $this->classResolver = new ClassResolver($classMap); $this->customTypeRegistry = new CustomTypeRegistry(); @@ -212,15 +209,13 @@ public function map( ?MappingContext $context = null, ?MappingConfiguration $configuration = null, ): mixed { - if ($context === null) { - $configuration = $configuration ?? $this->createDefaultConfiguration(); - $context = new MappingContext($json, $configuration->toOptions()); + if (!$context instanceof MappingContext) { + $configuration ??= $this->createDefaultConfiguration(); + $context = new MappingContext($json, $configuration->toOptions()); + } elseif (!$configuration instanceof MappingConfiguration) { + $configuration = MappingConfiguration::fromContext($context); } else { - if ($configuration === null) { - $configuration = MappingConfiguration::fromContext($context); - } else { - $context->replaceOptions($configuration->toOptions()); - } + $context->replaceOptions($configuration->toOptions()); } $resolvedClassName = $className === null @@ -418,7 +413,7 @@ private function createDefaultConfiguration(): MappingConfiguration $configuration = $configuration->withDefaultDateFormat($this->config->getDefaultDateFormat()); if ($this->config->shouldAllowScalarToObjectCasting()) { - $configuration = $configuration->withScalarToObjectCasting(true); + return $configuration->withScalarToObjectCasting(true); } return $configuration; diff --git a/src/JsonMapper/Attribute/ReplaceProperty.php b/src/JsonMapper/Attribute/ReplaceProperty.php index d4e5af3..cff49a6 100644 --- a/src/JsonMapper/Attribute/ReplaceProperty.php +++ b/src/JsonMapper/Attribute/ReplaceProperty.php @@ -17,11 +17,11 @@ * Attribute used to instruct the mapper to rename a JSON field. */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class ReplaceProperty +final readonly class ReplaceProperty { public function __construct( - public readonly string $value, - public readonly string $replaces, + public string $value, + public string $replaces, ) { } } diff --git a/src/JsonMapper/Collection/CollectionDocBlockTypeResolver.php b/src/JsonMapper/Collection/CollectionDocBlockTypeResolver.php index 63a08e2..cb0cbd0 100644 --- a/src/JsonMapper/Collection/CollectionDocBlockTypeResolver.php +++ b/src/JsonMapper/Collection/CollectionDocBlockTypeResolver.php @@ -28,14 +28,10 @@ final class CollectionDocBlockTypeResolver { private DocBlockFactoryInterface $docBlockFactory; - private ContextFactory $contextFactory; - - private PhpDocTypeHelper $phpDocTypeHelper; - public function __construct( ?DocBlockFactoryInterface $docBlockFactory = null, - ?ContextFactory $contextFactory = null, - ?PhpDocTypeHelper $phpDocTypeHelper = null, + private ContextFactory $contextFactory = new ContextFactory(), + private PhpDocTypeHelper $phpDocTypeHelper = new PhpDocTypeHelper(), ) { if (!class_exists(DocBlockFactory::class)) { throw new LogicException( @@ -46,9 +42,7 @@ public function __construct( ); } - $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance(); - $this->contextFactory = $contextFactory ?? new ContextFactory(); - $this->phpDocTypeHelper = $phpDocTypeHelper ?? new PhpDocTypeHelper(); + $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance(); } /** diff --git a/src/JsonMapper/Context/MappingContext.php b/src/JsonMapper/Context/MappingContext.php index b9ecf8b..352fe78 100644 --- a/src/JsonMapper/Context/MappingContext.php +++ b/src/JsonMapper/Context/MappingContext.php @@ -24,13 +24,19 @@ */ final class MappingContext { - public const OPTION_STRICT_MODE = 'strict_mode'; - public const OPTION_COLLECT_ERRORS = 'collect_errors'; - public const OPTION_TREAT_EMPTY_STRING_AS_NULL = 'empty_string_is_null'; - public const OPTION_IGNORE_UNKNOWN_PROPERTIES = 'ignore_unknown_properties'; - public const OPTION_TREAT_NULL_AS_EMPTY_COLLECTION = 'treat_null_as_empty_collection'; - public const OPTION_DEFAULT_DATE_FORMAT = 'default_date_format'; - public const OPTION_ALLOW_SCALAR_TO_OBJECT_CASTING = 'allow_scalar_to_object_casting'; + public const string OPTION_STRICT_MODE = 'strict_mode'; + + public const string OPTION_COLLECT_ERRORS = 'collect_errors'; + + public const string OPTION_TREAT_EMPTY_STRING_AS_NULL = 'empty_string_is_null'; + + public const string OPTION_IGNORE_UNKNOWN_PROPERTIES = 'ignore_unknown_properties'; + + public const string OPTION_TREAT_NULL_AS_EMPTY_COLLECTION = 'treat_null_as_empty_collection'; + + public const string OPTION_DEFAULT_DATE_FORMAT = 'default_date_format'; + + public const string OPTION_ALLOW_SCALAR_TO_OBJECT_CASTING = 'allow_scalar_to_object_casting'; /** * @var list diff --git a/src/JsonMapper/Context/MappingError.php b/src/JsonMapper/Context/MappingError.php index 6bcc624..568bcbe 100644 --- a/src/JsonMapper/Context/MappingError.php +++ b/src/JsonMapper/Context/MappingError.php @@ -16,12 +16,12 @@ /** * Represents a collected mapping error. */ -final class MappingError +final readonly class MappingError { public function __construct( - private readonly string $path, - private readonly string $message, - private readonly ?MappingException $exception = null, + private string $path, + private string $message, + private ?MappingException $exception = null, ) { } diff --git a/src/JsonMapper/Report/MappingReport.php b/src/JsonMapper/Report/MappingReport.php index 9195094..886a41e 100644 --- a/src/JsonMapper/Report/MappingReport.php +++ b/src/JsonMapper/Report/MappingReport.php @@ -16,12 +16,12 @@ /** * Represents the result of collecting mapping errors. */ -final class MappingReport +final readonly class MappingReport { /** * @param list $errors */ - public function __construct(private readonly array $errors) + public function __construct(private array $errors) { } diff --git a/src/JsonMapper/Report/MappingResult.php b/src/JsonMapper/Report/MappingResult.php index 0861e22..c6056d0 100644 --- a/src/JsonMapper/Report/MappingResult.php +++ b/src/JsonMapper/Report/MappingResult.php @@ -14,11 +14,11 @@ /** * Represents the outcome of a mapping operation and its report. */ -final class MappingResult +final readonly class MappingResult { public function __construct( - private readonly mixed $value, - private readonly MappingReport $report, + private mixed $value, + private MappingReport $report, ) { } diff --git a/src/JsonMapper/Type/TypeResolver.php b/src/JsonMapper/Type/TypeResolver.php index d004e8b..a6acbc8 100644 --- a/src/JsonMapper/Type/TypeResolver.php +++ b/src/JsonMapper/Type/TypeResolver.php @@ -28,7 +28,7 @@ */ final class TypeResolver { - private const CACHE_KEY_PREFIX = 'jsonmapper.property_type.'; + private const string CACHE_KEY_PREFIX = 'jsonmapper.property_type.'; private BuiltinType $defaultType; @@ -61,11 +61,7 @@ public function resolve(string $className, string $propertyName): Type $type = $this->resolveFromReflection($className, $propertyName); } - if ($type instanceof Type) { - $resolved = $this->normalizeType($type); - } else { - $resolved = $this->defaultType; - } + $resolved = $type instanceof Type ? $this->normalizeType($type) : $this->defaultType; $this->storeCachedType($className, $propertyName, $resolved); @@ -91,7 +87,7 @@ private function normalizeType(Type $type): Type */ private function getCachedType(string $className, string $propertyName): ?Type { - if ($this->cache === null) { + if (!$this->cache instanceof CacheItemPoolInterface) { return null; } @@ -119,7 +115,7 @@ private function getCachedType(string $className, string $propertyName): ?Type */ private function storeCachedType(string $className, string $propertyName, Type $type): void { - if ($this->cache === null) { + if (!$this->cache instanceof CacheItemPoolInterface) { return; } diff --git a/src/JsonMapper/Value/Strategy/BuiltinValueConversionStrategy.php b/src/JsonMapper/Value/Strategy/BuiltinValueConversionStrategy.php index 74f65d7..4eacbbb 100644 --- a/src/JsonMapper/Value/Strategy/BuiltinValueConversionStrategy.php +++ b/src/JsonMapper/Value/Strategy/BuiltinValueConversionStrategy.php @@ -16,7 +16,6 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\TypeIdentifier; -use Traversable; use function assert; use function filter_var; @@ -169,7 +168,7 @@ private function isCompatibleValue(mixed $value, TypeIdentifier $identifier): bo 'array' => is_array($value), 'object' => is_object($value), 'callable' => is_callable($value), - 'iterable' => is_array($value) || $value instanceof Traversable, + 'iterable' => is_iterable($value), 'null' => $value === null, default => true, }; diff --git a/src/JsonMapper/Value/Strategy/DateTimeValueConversionStrategy.php b/src/JsonMapper/Value/Strategy/DateTimeValueConversionStrategy.php index 077e318..ac60f44 100644 --- a/src/JsonMapper/Value/Strategy/DateTimeValueConversionStrategy.php +++ b/src/JsonMapper/Value/Strategy/DateTimeValueConversionStrategy.php @@ -18,6 +18,7 @@ use MagicSunday\JsonMapper\Context\MappingContext; use MagicSunday\JsonMapper\Exception\TypeMismatchException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; use function get_debug_type; use function is_a; @@ -35,7 +36,7 @@ public function supports(mixed $value, Type $type, MappingContext $context): boo { $objectType = $this->extractObjectType($type); - if ($objectType === null) { + if (!$objectType instanceof ObjectType) { return false; } diff --git a/src/JsonMapper/Value/Strategy/EnumValueConversionStrategy.php b/src/JsonMapper/Value/Strategy/EnumValueConversionStrategy.php index 0e7b553..1672d55 100644 --- a/src/JsonMapper/Value/Strategy/EnumValueConversionStrategy.php +++ b/src/JsonMapper/Value/Strategy/EnumValueConversionStrategy.php @@ -15,6 +15,7 @@ use MagicSunday\JsonMapper\Context\MappingContext; use MagicSunday\JsonMapper\Exception\TypeMismatchException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; use ValueError; use function enum_exists; @@ -34,7 +35,7 @@ public function supports(mixed $value, Type $type, MappingContext $context): boo { $objectType = $this->extractObjectType($type); - if ($objectType === null) { + if (!$objectType instanceof ObjectType) { return false; } diff --git a/src/JsonMapper/Value/Strategy/ObjectValueConversionStrategy.php b/src/JsonMapper/Value/Strategy/ObjectValueConversionStrategy.php index 5640f46..717ff35 100644 --- a/src/JsonMapper/Value/Strategy/ObjectValueConversionStrategy.php +++ b/src/JsonMapper/Value/Strategy/ObjectValueConversionStrategy.php @@ -51,14 +51,12 @@ public function convert(mixed $value, Type $type, MappingContext $context): mixe $className = $this->resolveClassName($type); $resolvedClass = $this->classResolver->resolve($className, $value, $context); - if (($value !== null) && !is_array($value) && !is_object($value)) { - if (!$context->shouldAllowScalarToObjectCasting()) { - $exception = new TypeMismatchException($context->getPath(), $resolvedClass, get_debug_type($value)); - $context->recordException($exception); - - if ($context->isStrictMode()) { - throw $exception; - } + if ($value !== null && !is_array($value) && !is_object($value) && !$context->shouldAllowScalarToObjectCasting()) { + $exception = new TypeMismatchException($context->getPath(), $resolvedClass, get_debug_type($value)); + $context->recordException($exception); + + if ($context->isStrictMode()) { + throw $exception; } } diff --git a/tests/TestCase.php b/tests/TestCase.php index cd26409..19eac96 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -48,7 +48,7 @@ protected function getJsonMapper(array $classMap = [], ?JsonMapperConfig $config new CamelCasePropertyNameConverter(), $classMap, null, - $config, + $config ?? new JsonMapperConfig(), ); }