diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e2b07c..558dc725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * test: test array cast to object mapping * feat(`Context`): `with()` not accepts multiple argument. * build: Deinternalize `ObjectCacheFactory` +* fix(`PresetMapping`): Support proxied classes, add tests. ## 1.0.0 diff --git a/config/tests.php b/config/tests.php index 144bbc80..df4175d4 100644 --- a/config/tests.php +++ b/config/tests.php @@ -11,6 +11,7 @@ * that was distributed with this source code. */ +use Rekalogika\Mapper\MapperInterface; use Rekalogika\Mapper\Tests\Common\TestKernel; use Rekalogika\Mapper\Tests\Fixtures\Money\MoneyToMoneyDtoTransformer; use Rekalogika\Mapper\Tests\Fixtures\ObjectMapper\MoneyObjectMapper; @@ -21,6 +22,7 @@ use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithConstructorWithoutClassAttribute; use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithExtraArguments; use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithoutClassAttribute; +use Rekalogika\Mapper\Tests\Fixtures\RememberingMapper\RememberingMapper; use Rekalogika\Mapper\Tests\Fixtures\TransformerOverride\OverrideTransformer; use Rekalogika\Mapper\Transformer\Implementation\ScalarToScalarTransformer; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -58,4 +60,10 @@ ->args([ '$transformer' => service(ScalarToScalarTransformer::class), ]); + + $services->set(RememberingMapper::class) + ->args([ + '$decorated' => service(MapperInterface::class), + '$objectCacheFactory' => service('rekalogika.mapper.object_cache_factory'), + ]); }; diff --git a/src/Transformer/Context/PresetMapping.php b/src/Transformer/Context/PresetMapping.php index 3fa9e22a..2882037b 100644 --- a/src/Transformer/Context/PresetMapping.php +++ b/src/Transformer/Context/PresetMapping.php @@ -13,10 +13,11 @@ namespace Rekalogika\Mapper\Transformer\Context; -use Rekalogika\Mapper\ObjectCache\ObjectCache; use Rekalogika\Mapper\Transformer\Exception\PresetMappingNotFound; -use Rekalogika\Mapper\Transformer\Model\SplObjectStorageWrapper; +/** + * Contains preset object to object mapping, used by `PresetTransformer` + */ final readonly class PresetMapping { /** @@ -27,7 +28,7 @@ /** * @param iterable> $mappings */ - public function __construct(iterable $mappings) + public function __construct(iterable $mappings = []) { /** * @var \WeakMap> @@ -47,44 +48,23 @@ public function __construct(iterable $mappings) $this->mappings = $weakMap; } - public static function fromObjectCache(ObjectCache $objectCache): self + public function mergeFrom(self $presetMapping): void { - $objectCacheWeakMap = $objectCache->getInternalMapping(); - - /** @var SplObjectStorageWrapper> */ - $presetMapping = new SplObjectStorageWrapper(new \SplObjectStorage()); - /** * @var object $source * @var \ArrayObject $classToTargetMapping */ - foreach ($objectCacheWeakMap as $source => $classToTargetMapping) { - $newTargetClass = $source::class; - /** @var object */ - $newTarget = $source; - - /** - * @var string $targetClass - * @var object $target - */ - foreach ($classToTargetMapping as $targetClass => $target) { - if (!class_exists($targetClass)) { - continue; - } - - $newSource = $target; - - if (!$presetMapping->offsetExists($newSource)) { - /** @var \ArrayObject */ - $arrayObject = new \ArrayObject(); - $presetMapping->offsetSet($newSource, $arrayObject); - } - - $presetMapping->offsetGet($newSource)?->offsetSet($newTargetClass, $newTarget); + foreach ($presetMapping->mappings as $source => $classToTargetMapping) { + if (!$this->mappings->offsetExists($source)) { + /** @var \ArrayObject */ + $arrayObject = new \ArrayObject(); + $this->mappings->offsetSet($source, $arrayObject); } - } - return new self($presetMapping); + foreach ($classToTargetMapping as $class => $target) { + $this->mappings->offsetGet($source)?->offsetSet($class, $target); + } + } } /** diff --git a/src/Transformer/Context/PresetMappingFactory.php b/src/Transformer/Context/PresetMappingFactory.php new file mode 100644 index 00000000..762b21c1 --- /dev/null +++ b/src/Transformer/Context/PresetMappingFactory.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\Context; + +use Rekalogika\Mapper\ObjectCache\ObjectCache; +use Rekalogika\Mapper\Transformer\Model\SplObjectStorageWrapper; +use Rekalogika\Mapper\Util\ClassUtil; + +final readonly class PresetMappingFactory +{ + private function __construct() + { + } + + public static function fromObjectCache(ObjectCache $objectCache): PresetMapping + { + $objectCacheWeakMap = $objectCache->getInternalMapping(); + + /** @var SplObjectStorageWrapper> */ + $presetMapping = new SplObjectStorageWrapper(new \SplObjectStorage()); + + /** + * @var object $source + * @var \ArrayObject $classToTargetMapping + */ + foreach ($objectCacheWeakMap as $source => $classToTargetMapping) { + $newTargetClass = ClassUtil::determineRealClassFromPossibleProxy($source::class); + /** @var object */ + $newTarget = $source; + + /** + * @var string $targetClass + * @var object $target + */ + foreach ($classToTargetMapping as $targetClass => $target) { + if (!class_exists($targetClass)) { + continue; + } + + $newSource = $target; + + if (!$presetMapping->offsetExists($newSource)) { + /** @var \ArrayObject */ + $arrayObject = new \ArrayObject(); + $presetMapping->offsetSet($newSource, $arrayObject); + } + + $presetMapping->offsetGet($newSource)?->offsetSet($newTargetClass, $newTarget); + } + } + + return new PresetMapping($presetMapping); + } +} diff --git a/tests/Fixtures/RememberingMapper/RememberingMapper.php b/tests/Fixtures/RememberingMapper/RememberingMapper.php new file mode 100644 index 00000000..89adc4a2 --- /dev/null +++ b/tests/Fixtures/RememberingMapper/RememberingMapper.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\RememberingMapper; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +use Rekalogika\Mapper\MapperInterface; +use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; +use Rekalogika\Mapper\Transformer\Context\PresetMapping; +use Rekalogika\Mapper\Transformer\Context\PresetMappingFactory; +use Symfony\Contracts\Service\ResetInterface; + +class RememberingMapper implements MapperInterface, ResetInterface +{ + private PresetMapping $presetMapping; + + public function __construct( + private MapperInterface $decorated, + private ObjectCacheFactoryInterface $objectCacheFactory + ) { + $this->presetMapping = new PresetMapping(); + } + + public function reset(): void + { + $this->presetMapping = new PresetMapping(); + } + + public function map(object $source, object|string $target, ?Context $context = null): object + { + $objectCache = $this->objectCacheFactory->createObjectCache(); + + if ($context === null) { + $context = Context::create(); + } + + $context = $context->with($objectCache, $this->presetMapping); + + $result = $this->decorated->map($source, $target, $context); + + if (is_object($target)) { + $target = $target::class; + } + + if (!$result instanceof $target) { + throw new UnexpectedValueException(sprintf('Expected instance of "%s", got "%s"', $target, get_class($result))); + } + + $newPresetMapping = PresetMappingFactory::fromObjectCache($objectCache); + $this->presetMapping->mergeFrom($newPresetMapping); + + return $result; + } +} diff --git a/tests/IntegrationTest/PresetMappingTest.php b/tests/IntegrationTest/PresetMappingTest.php index 8db69844..24a5e33b 100644 --- a/tests/IntegrationTest/PresetMappingTest.php +++ b/tests/IntegrationTest/PresetMappingTest.php @@ -17,7 +17,7 @@ use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto; -use Rekalogika\Mapper\Transformer\Context\PresetMapping; +use Rekalogika\Mapper\Transformer\Context\PresetMappingFactory; use Rekalogika\Mapper\TypeResolver\TypeResolverInterface; use Rekalogika\Mapper\Util\TypeFactory; @@ -41,11 +41,40 @@ public function testFromObjectCache(): void $objectCache->saveTarget($source, $targetType, $target); - $presetMapping = PresetMapping::fromObjectCache($objectCache); + $presetMapping = PresetMappingFactory::fromObjectCache($objectCache); $result = $presetMapping->findResult($target, $source::class); $this->assertSame($source, $result); } + public function testMerge(): void + { + $objectCache = $this->createObjectCache(); + + $source = new ObjectWithScalarProperties(); + $targetType = TypeFactory::objectOfClass(ObjectWithScalarProperties::class); + $target = new ObjectWithScalarPropertiesDto(); + + $objectCache->saveTarget($source, $targetType, $target); + + $presetMapping = PresetMappingFactory::fromObjectCache($objectCache); + + $source2 = new ObjectWithScalarProperties(); + $targetType2 = TypeFactory::objectOfClass(ObjectWithScalarProperties::class); + $target2 = new ObjectWithScalarPropertiesDto(); + + $objectCache->saveTarget($source2, $targetType2, $target2); + + $presetMapping2 = PresetMappingFactory::fromObjectCache($objectCache); + + $presetMapping->mergeFrom($presetMapping2); + + $result = $presetMapping->findResult($target, $source::class); + $this->assertSame($source, $result); + + $result = $presetMapping->findResult($target2, $source2::class); + $this->assertSame($source2, $result); + } + } diff --git a/tests/IntegrationTest/RememberingMapperTest.php b/tests/IntegrationTest/RememberingMapperTest.php new file mode 100644 index 00000000..8d22c5cd --- /dev/null +++ b/tests/IntegrationTest/RememberingMapperTest.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 Rekalogika\Mapper\Tests\IntegrationTest; + +use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\RememberingMapper\RememberingMapper; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesWithNullContents; +use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithFloatPropertiesDto; +use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithIntPropertiesDto; + +class RememberingMapperTest extends FrameworkTestCase +{ + public function testMapping(): void + { + $mapper = $this->get(RememberingMapper::class); + + $source1 = new ObjectWithScalarProperties(); + $target1 = ObjectWithFloatPropertiesDto::class; + $result1 = $mapper->map($source1, $target1); + + $source2 = new ObjectWithScalarPropertiesWithNullContents(); + $target2 = ObjectWithIntPropertiesDto::class; + $result2 = $mapper->map($source2, $target2); + + $shouldbeSource1 = $mapper->map($result1, $source1::class); + $shouldbeSource2 = $mapper->map($result2, $source2::class); + + $this->assertEquals($source1, $shouldbeSource1); + $this->assertEquals($source2, $shouldbeSource2); + } +}