diff --git a/CHANGELOG.md b/CHANGELOG.md index bb83d6be..e231d43b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * feat: `Map` now accepts false, for the more expressive `#[Map(false)]` * refactor: remove property processor for easier future optimization +* fix: turn `NewInstanceReturnedButCannotBeSetOnTargetException` into a warning ## 1.13.1 diff --git a/config/services.php b/config/services.php index 1d336036..fda420c8 100644 --- a/config/services.php +++ b/config/services.php @@ -389,6 +389,7 @@ '$subMapperFactory' => service('rekalogika.mapper.sub_mapper.factory'), '$proxyFactory' => service('rekalogika.mapper.proxy.factory'), '$propertyAccessor' => service(PropertyAccessorInterface::class), + '$logger' => service(LoggerInterface::class), ]); # sub mapper diff --git a/src/MapperFactory.php b/src/MapperFactory.php index c87a7cc9..0bcbf5aa 100644 --- a/src/MapperFactory.php +++ b/src/MapperFactory.php @@ -15,6 +15,8 @@ use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Ramsey\Uuid\UuidInterface; use Rekalogika\Mapper\Command\MappingCommand; use Rekalogika\Mapper\Command\TryCommand; @@ -209,6 +211,8 @@ class MapperFactory private ?Application $application = null; + private LoggerInterface $logger; + /** * @param array $additionalTransformers */ @@ -219,7 +223,10 @@ public function __construct( private readonly ?NormalizerInterface $normalizer = null, private readonly ?DenormalizerInterface $denormalizer = null, private readonly CacheItemPoolInterface $propertyInfoExtractorCache = new ArrayAdapter(), - ) {} + ?LoggerInterface $logger = null, + ) { + $this->logger = $logger ?? new NullLogger(); + } /** * @param class-string $sourceClass @@ -907,6 +914,7 @@ protected function getObjectProcessorFactory(): ObjectProcessorFactoryInterface subMapperFactory: $this->getSubMapperFactory(), proxyFactory: $this->getProxyFactory(), propertyAccessor: $this->getPropertyAccessor(), + logger: $this->logger, ); } diff --git a/src/Transformer/Exception/NewInstanceReturnedButCannotBeSetOnTargetException.php b/src/Transformer/Exception/NewInstanceReturnedButCannotBeSetOnTargetException.php deleted file mode 100644 index 69195343..00000000 --- a/src/Transformer/Exception/NewInstanceReturnedButCannotBeSetOnTargetException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -namespace Rekalogika\Mapper\Transformer\Exception; - -use Rekalogika\Mapper\Context\Context; - -class NewInstanceReturnedButCannotBeSetOnTargetException extends NotMappableValueException -{ - public function __construct( - mixed $target, - string $propertyName, - ?\Throwable $previous = null, - ?Context $context = null, - ) { - parent::__construct( - message: \sprintf( - 'Transformation of property "%s" on object type "%s" results in a different object instance from the original instance, but the new instance cannot be set on the target object. You may wish to add a setter method to the target class.', - $propertyName, - get_debug_type($target), - ), - previous: $previous, - context: $context, - ); - } -} diff --git a/src/Transformer/Processor/ObjectProcessor/DefaultObjectProcessorFactory.php b/src/Transformer/Processor/ObjectProcessor/DefaultObjectProcessorFactory.php index 52534072..9a805ef1 100644 --- a/src/Transformer/Processor/ObjectProcessor/DefaultObjectProcessorFactory.php +++ b/src/Transformer/Processor/ObjectProcessor/DefaultObjectProcessorFactory.php @@ -14,6 +14,7 @@ namespace Rekalogika\Mapper\Transformer\Processor\ObjectProcessor; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; @@ -34,6 +35,7 @@ public function __construct( private readonly SubMapperFactoryInterface $subMapperFactory, private readonly ProxyFactoryInterface $proxyFactory, private readonly PropertyAccessorInterface $propertyAccessor, + private readonly LoggerInterface $logger, ) {} public function getObjectProcessor( @@ -46,6 +48,7 @@ public function getObjectProcessor( subMapperFactory: $this->subMapperFactory, proxyFactory: $this->proxyFactory, propertyAccessor: $this->propertyAccessor, + logger: $this->logger, ); } } diff --git a/src/Transformer/Processor/ObjectProcessor/ObjectProcessor.php b/src/Transformer/Processor/ObjectProcessor/ObjectProcessor.php index c0097322..2f8e5e20 100644 --- a/src/Transformer/Processor/ObjectProcessor/ObjectProcessor.php +++ b/src/Transformer/Processor/ObjectProcessor/ObjectProcessor.php @@ -14,6 +14,7 @@ namespace Rekalogika\Mapper\Transformer\Processor\ObjectProcessor; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use Rekalogika\Mapper\Context\Context; use Rekalogika\Mapper\Context\ExtraTargetValues; use Rekalogika\Mapper\Context\MapperOptions; @@ -26,7 +27,6 @@ use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; -use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; use Rekalogika\Mapper\Transformer\Exception\UnableToReadException; use Rekalogika\Mapper\Transformer\Exception\UnableToWriteException; use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; @@ -56,6 +56,7 @@ public function __construct( private SubMapperFactoryInterface $subMapperFactory, private ProxyFactoryInterface $proxyFactory, private PropertyAccessorInterface $propertyAccessor, + private LoggerInterface $logger, ) {} public function transform( @@ -989,15 +990,17 @@ public function writeTargetProperty( $visibility !== Visibility::Public || $writeMode === WriteMode::None ) { - if ($silentOnError) { - return $target; + if (!$silentOnError) { + $this->logger->warning( + 'Transformation of property "{property}" on target class "{class}" results in a different object instance from the original instance, but the new instance cannot be set on the target object. To fix the problem, you may 1. make the property public, 2. add a setter method for the property, or 3. add "#[Map(false)]" attribute on the property to skip the mapping.', + [ + 'property' => $metadata->getTargetProperty(), + 'class' => get_debug_type($target), + ], + ); } - throw new NewInstanceReturnedButCannotBeSetOnTargetException( - target: $target, - propertyName: $metadata->getTargetProperty(), - context: $context, - ); + return $target; } if ($accessorName === null) { diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php deleted file mode 100644 index 3fc01ea1..00000000 --- a/src/Transformer/Util/ReaderWriter.php +++ /dev/null @@ -1,286 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -namespace Rekalogika\Mapper\Transformer\Util; - -use Rekalogika\Mapper\Context\Context; -use Rekalogika\Mapper\Exception\UnexpectedValueException; -use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; -use Rekalogika\Mapper\Transformer\Exception\UnableToReadException; -use Rekalogika\Mapper\Transformer\Exception\UnableToWriteException; -use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; -use Rekalogika\Mapper\Transformer\Model\AdderRemoverProxy; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ReadMode; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Visibility; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\WriteMode; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * @internal - * @todo inject logger - */ -final readonly class ReaderWriter -{ - public function __construct( - private PropertyAccessorInterface $propertyAccessor, - ) {} - - /** - * @throws UninitializedSourcePropertyException - * @throws UnableToReadException - */ - public function readSourceProperty( - object $source, - PropertyMappingMetadata $propertyMapping, - Context $context, - ): mixed { - $property = $propertyMapping->getSourceProperty(); - - if ($property === null) { - return null; - } - - if ($propertyMapping->getSourceReadVisibility() !== Visibility::Public) { - throw new UnableToReadException( - $source, - $property, - context: $context, - ); - } - - try { - $accessorName = $propertyMapping->getSourceReadName(); - $mode = $propertyMapping->getSourceReadMode(); - - if ($accessorName === null) { - throw new UnexpectedValueException('AccessorName is null', context: $context); - } - - if ($mode === ReadMode::Property) { - return $source->{$accessorName}; - } elseif ($mode === ReadMode::Method) { - /** @psalm-suppress MixedMethodCall */ - return $source->{$accessorName}(); - } elseif ($mode === ReadMode::PropertyPath) { - return $this->propertyAccessor - ->getValue($source, $accessorName); - } elseif ($mode === ReadMode::DynamicProperty) { - $errorHandler = static function ( - int $errno, - string $errstr, - string $errfile, - int $errline, - ) use ($accessorName): bool { - if (str_starts_with($errstr, 'Undefined property')) { - restore_error_handler(); - throw new UninitializedSourcePropertyException($accessorName); - } - - return false; - }; - - set_error_handler($errorHandler); - /** @var mixed */ - $result = $source->{$accessorName}; - restore_error_handler(); - - return $result; - } - - return null; - } catch (\Error $e) { - $message = $e->getMessage(); - - if ( - str_contains($message, 'must not be accessed before initialization') - || str_contains($message, 'Cannot access uninitialized non-nullable property') - ) { - throw new UninitializedSourcePropertyException($property); - } - - throw new UnableToReadException( - $source, - $property, - context: $context, - previous: $e, - ); - } catch (\BadMethodCallException) { - throw new UninitializedSourcePropertyException($property); - } - } - - /** - * @throws UnableToReadException - */ - public function readTargetProperty( - object $target, - PropertyMappingMetadata $propertyMapping, - Context $context, - ): mixed { - if ( - $propertyMapping->getTargetSetterWriteMode() === WriteMode::AdderRemover - && $propertyMapping->getTargetSetterWriteVisibility() === Visibility::Public - ) { - if ( - $propertyMapping->getTargetRemoverWriteVisibility() === Visibility::Public - ) { - $removerMethodName = $propertyMapping->getTargetRemoverWriteName(); - } else { - $removerMethodName = null; - } - - return new AdderRemoverProxy( - hostObject: $target, - getterMethodName: $propertyMapping->getTargetReadName(), - adderMethodName: $propertyMapping->getTargetSetterWriteName(), - removerMethodName: $removerMethodName, - ); - } - - if ($propertyMapping->getTargetReadVisibility() !== Visibility::Public) { - return null; - } - - try { - $accessorName = $propertyMapping->getTargetReadName(); - $readMode = $propertyMapping->getTargetReadMode(); - - if ($accessorName === null) { - throw new UnexpectedValueException('AccessorName is null', context: $context); - } - - if ($readMode === ReadMode::Property) { - return $target->{$accessorName}; - } elseif ($readMode === ReadMode::Method) { - /** @psalm-suppress MixedMethodCall */ - return $target->{$accessorName}(); - } elseif ($readMode === ReadMode::PropertyPath) { - return $this->propertyAccessor - ->getValue($target, $accessorName); - } elseif ($readMode === ReadMode::DynamicProperty) { - return $target->{$accessorName} ?? null; - } - - return null; - } catch (\Error $e) { - $message = $e->getMessage(); - - if ( - str_contains($message, 'must not be accessed before initialization') - || str_contains($message, 'Cannot access uninitialized non-nullable property') - ) { - return null; - } - - throw new UnableToReadException( - $target, - $propertyMapping->getTargetProperty(), - context: $context, - previous: $e, - ); - } - } - - /** - * @throws UnableToWriteException - */ - public function writeTargetProperty( - object $target, - PropertyMappingMetadata $propertyMapping, - mixed $value, - Context $context, - bool $silentOnError, - ): object { - $accessorName = $propertyMapping->getTargetSetterWriteName(); - $writeMode = $propertyMapping->getTargetSetterWriteMode(); - $visibility = $propertyMapping->getTargetSetterWriteVisibility(); - - if ( - $visibility !== Visibility::Public - || $writeMode === WriteMode::None - ) { - if ($silentOnError) { - return $target; - } - - throw new NewInstanceReturnedButCannotBeSetOnTargetException( - $target, - $propertyMapping->getTargetProperty(), - context: $context, - ); - } - - if ($accessorName === null) { - throw new UnexpectedValueException('AccessorName is null', context: $context); - } - - try { - if ($writeMode === WriteMode::Property) { - $target->{$accessorName} = $value; - } elseif ($writeMode === WriteMode::Method) { - if ($propertyMapping->isTargetSetterVariadic()) { - if (!\is_array($value) && !$value instanceof \Traversable) { - $value = [$value]; - } - - /** @psalm-suppress MixedArgument */ - $value = iterator_to_array($value); - - /** - * @psalm-suppress MixedMethodCall - * @var mixed - */ - $result = $target->{$accessorName}(...$value); - } else { - /** - * @psalm-suppress MixedMethodCall - * @var mixed - */ - $result = $target->{$accessorName}($value); - } - - // if the setter returns the a value with the same type as the - // target object, we assume that the setter method is a fluent - // interface or an immutable setter, and we return the result - - if ( - \is_object($result) && is_a($result, $target::class, true) - ) { - return $result; - } - } elseif ($writeMode === WriteMode::AdderRemover) { - // noop - } elseif ($writeMode === WriteMode::PropertyPath) { - // PropertyAccessor might modify the target object - $temporaryTarget = $target; - - $this->propertyAccessor - ->setValue($temporaryTarget, $accessorName, $value); - } elseif ($writeMode === WriteMode::DynamicProperty) { - $target->{$accessorName} = $value; - } - } catch (\BadMethodCallException) { - return $target; - } catch (\Throwable $e) { - throw new UnableToWriteException( - $target, - $propertyMapping->getTargetProperty(), - context: $context, - previous: $e, - ); - } - - return $target; - } -} diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index bfd8f3a2..fce91e61 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -99,25 +99,25 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 153, 336 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 152, 335 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayAccessPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 203 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 202 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayInterfacePropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 239, 349 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 238, 348 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 52 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 51 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyWithoutTypeHintDto::class ); @@ -129,7 +129,7 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 254 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 253 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithCollectionPropertyDto::class ); @@ -159,19 +159,19 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 368 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 367 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayPropertyWithStringKey::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyDtoWithIntKey::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 171 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 170 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithCollectionProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayAccessPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 221 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 220 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithCollectionProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayInterfacePropertyDto::class ); @@ -189,7 +189,7 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 189 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 188 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithNullCollectionProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithNotNullArrayAccessPropertyDto::class ); @@ -201,55 +201,55 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 276 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 275 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithSplObjectStorageProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayAccessPropertyWithObjectKeyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 294 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 293 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithSplObjectStorageProperty::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 102, 307, 391 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 101, 306, 390 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayAccessPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 116, 321 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on lines 115, 320 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 82 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 81 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyWithCompatibleHintDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 67 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 66 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyWithoutTypeHintDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 432 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 429 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayWithGetterNoSetterDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 131 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 130 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithCollectionPropertyDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 408 + // tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php on line 407 source: \Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithTraversableProperties::class, target: \Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithCollectionWithGetterNoSetterDto::class ); @@ -267,32 +267,32 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/BasicMappingTest.php on line 34 + // tests/src/IntegrationTest/BasicMappingTest.php on line 33 // tests/src/IntegrationTest/IterableMapperTest.php on line 24 source: \Rekalogika\Mapper\Tests\Fixtures\Basic\Person::class, target: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/BasicMappingTest.php on line 43 + // tests/src/IntegrationTest/BasicMappingTest.php on line 42 source: \Rekalogika\Mapper\Tests\Fixtures\Basic\Person::class, target: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithoutAgeDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/BasicMappingTest.php on line 75 + // tests/src/IntegrationTest/BasicMappingTest.php on line 74 source: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithDogDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithDog::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/BasicMappingTest.php on line 103 + // tests/src/IntegrationTest/BasicMappingTest.php on line 99 source: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithDogDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithImmutableDogWithoutSetter::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/BasicMappingTest.php on line 51 + // tests/src/IntegrationTest/BasicMappingTest.php on line 50 source: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithoutAge::class, target: \Rekalogika\Mapper\Tests\Fixtures\Basic\PersonDto::class ); @@ -1061,37 +1061,37 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on line 35 + // tests/src/IntegrationTest/WitherMethodTest.php on line 34 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithFluentSetter::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on lines 71, 101 + // tests/src/IntegrationTest/WitherMethodTest.php on lines 70, 100 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithImmutableSetter::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on line 59 + // tests/src/IntegrationTest/WitherMethodTest.php on line 58 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithSetterReturningForeignObject::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on line 47 + // tests/src/IntegrationTest/WitherMethodTest.php on line 46 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithVoidSetter::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on line 83 + // tests/src/IntegrationTest/WitherMethodTest.php on line 82 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithWither::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/WitherMethodTest.php on line 95 + // tests/src/IntegrationTest/WitherMethodTest.php on line 92 source: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithoutSetterDto::class ); diff --git a/tests/src/Common/FrameworkTestCase.php b/tests/src/Common/FrameworkTestCase.php index 77fbf0ca..5320fd70 100644 --- a/tests/src/Common/FrameworkTestCase.php +++ b/tests/src/Common/FrameworkTestCase.php @@ -16,11 +16,13 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\LoggerInterface; use Rekalogika\Mapper\Context\Context; use Rekalogika\Mapper\Debug\MapperDataCollector; use Rekalogika\Mapper\Debug\TraceableTransformer; use Rekalogika\Mapper\IterableMapperInterface; use Rekalogika\Mapper\MapperInterface; +use Rekalogika\Mapper\Tests\Services\TestLogger; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\VarExporter\LazyObjectInterface; @@ -123,4 +125,11 @@ public function getDataCollector(): MapperDataCollector return $result; } + + public function assertLogContains(string $message): void + { + $logger = static::getContainer()->get(LoggerInterface::class); + $this->assertInstanceOf(TestLogger::class, $logger); + $this->assertTrue($logger->isInMessage($message), 'Log message not found: ' . $message); + } } diff --git a/tests/src/IntegrationTest/BasicMappingTest.php b/tests/src/IntegrationTest/BasicMappingTest.php index 2e22ddf4..b885091b 100644 --- a/tests/src/IntegrationTest/BasicMappingTest.php +++ b/tests/src/IntegrationTest/BasicMappingTest.php @@ -24,7 +24,6 @@ use Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithImmutableDogWithoutSetter; use Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithoutAge; use Rekalogika\Mapper\Tests\Fixtures\Basic\PersonWithoutAgeDto; -use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; class BasicMappingTest extends FrameworkTestCase { @@ -82,9 +81,6 @@ public function testSetterNotCalledIfValueDoesntChange(): void public function testSkippingImmutableEntityWithNoSetterOnTarget(): void { - // old behavior no longer supported, this throws an exception now - $this->expectException(NewInstanceReturnedButCannotBeSetOnTargetException::class); - // source $personDto = new PersonWithDogDto(); $dogDto = new DogDto(); @@ -104,5 +100,7 @@ public function testSkippingImmutableEntityWithNoSetterOnTarget(): void $this->assertEquals('John', $person->getName()); $this->assertEquals('Rex', $person->getDog()->getName()); + + $this->assertLogContains('results in a different object instance from the original instance'); } } diff --git a/tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php b/tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php index 02f1aaa4..9fc93003 100644 --- a/tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php +++ b/tests/src/IntegrationTest/TraversableToArrayAccessMappingTest.php @@ -36,7 +36,6 @@ use Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithNotNullArrayAccessPropertyDto; use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto; -use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; use Rekalogika\Mapper\Transformer\Model\HashTable; use Rekalogika\Mapper\Transformer\Model\LazyArray; @@ -424,11 +423,11 @@ public function testMappingToCollectionWithGetterButNoSetter(): void public function testMappingToArrayWithGetterButNoSetter(): void { - $this->expectException(NewInstanceReturnedButCannotBeSetOnTargetException::class); - $source = new ObjectWithTraversableProperties(); $target = new ObjectWithArrayWithGetterNoSetterDto(); $result = $this->mapper->map($source, $target); + + $this->assertLogContains('results in a different object instance from the original instance'); } } diff --git a/tests/src/IntegrationTest/WitherMethodTest.php b/tests/src/IntegrationTest/WitherMethodTest.php index 79898a3a..91d0079d 100644 --- a/tests/src/IntegrationTest/WitherMethodTest.php +++ b/tests/src/IntegrationTest/WitherMethodTest.php @@ -21,7 +21,6 @@ use Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithVoidSetter as ParentObjectWithObjectWithVoidSetterDto; use Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithObjectWithWither; use Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ParentObjectWithoutSetterDto; -use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; use Symfony\Component\VarExporter\LazyObjectInterface; class WitherMethodTest extends FrameworkTestCase @@ -88,11 +87,11 @@ public function testWither(): void public function testChildImmutableSetterWithoutSetterOnParent(): void { - $this->expectException(NewInstanceReturnedButCannotBeSetOnTargetException::class); - $source = new ParentObject(); $target = new ParentObjectWithoutSetterDto(); $this->mapper->map($source, $target); + + $this->assertLogContains('results in a different object instance from the original instance'); } public function testImmutableSetterWithProxy(): void diff --git a/tests/src/Services/TestLogger.php b/tests/src/Services/TestLogger.php index 8c48fcfa..affb92b1 100644 --- a/tests/src/Services/TestLogger.php +++ b/tests/src/Services/TestLogger.php @@ -16,15 +16,37 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; +use Symfony\Contracts\Service\ResetInterface; #[AsDecorator(LoggerInterface::class)] -class TestLogger implements LoggerInterface +class TestLogger implements LoggerInterface, ResetInterface { + /** + * @var list + */ + private array $messages = []; + public function __construct( #[AutowireDecorated()] private readonly LoggerInterface $logger, ) {} + public function reset() + { + $this->messages = []; + } + + public function isInMessage(string $string): bool + { + foreach ($this->messages as $message) { + if (str_contains($message, $string)) { + return true; + } + } + + return false; + } + private function isSuppressed(string|\Stringable $message): bool { return str_contains((string) $message, 'has a mapping involving an invalid class'); @@ -35,6 +57,7 @@ public function emergency(string|\Stringable $message, array $context = []): voi { if (!$this->isSuppressed($message)) { $this->logger->emergency($message, $context); + $this->messages[] = (string) $message; } } @@ -43,6 +66,7 @@ public function alert(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->alert($message, $context); + $this->messages[] = (string) $message; } } @@ -51,6 +75,7 @@ public function critical(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->critical($message, $context); + $this->messages[] = (string) $message; } } @@ -59,6 +84,7 @@ public function error(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->error($message, $context); + $this->messages[] = (string) $message; } } @@ -67,6 +93,7 @@ public function warning(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->warning($message, $context); + $this->messages[] = (string) $message; } } @@ -75,6 +102,7 @@ public function notice(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->notice($message, $context); + $this->messages[] = (string) $message; } } @@ -83,6 +111,7 @@ public function info(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->info($message, $context); + $this->messages[] = (string) $message; } } @@ -91,6 +120,7 @@ public function debug(string|\Stringable $message, array $context = []): void { if (!$this->isSuppressed($message)) { $this->logger->debug($message, $context); + $this->messages[] = (string) $message; } } @@ -99,6 +129,7 @@ public function log(mixed $level, string|\Stringable $message, array $context = { if (!$this->isSuppressed($message)) { $this->logger->log($level, $message, $context); + $this->messages[] = (string) $message; } } }