From bccb5e6ae3de6a3dc54e4d4a4746b7ce536d24e0 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:54:09 +0700 Subject: [PATCH] feat: `PresetTransformer` * feat: `PresetTransformer` --- CHANGELOG.md | 8 ++ README.md | 1 - config/services.php | 23 ++-- src/Debug/TraceData.php | 33 ++++- src/Debug/TraceableTransformer.php | 27 ++-- .../Implementation/MainTransformer.php | 4 + src/ObjectCache/ObjectCache.php | 25 ++++ src/Transformer/Context/PresetMapping.php | 117 ++++++++++++++++++ .../Exception/PresetMappingNotFound.php | 20 +++ .../Implementation/PresetTransformer.php | 67 ++++++++++ src/Transformer/TransformerInterface.php | 4 + templates/data_collector.html.twig | 4 +- tests/IntegrationTest/MappingTest.php | 14 ++- tests/IntegrationTest/PresetMappingTest.php | 51 ++++++++ 14 files changed, 370 insertions(+), 28 deletions(-) create mode 100644 src/Transformer/Context/PresetMapping.php create mode 100644 src/Transformer/Exception/PresetMappingNotFound.php create mode 100644 src/Transformer/Implementation/PresetTransformer.php create mode 100644 tests/IntegrationTest/PresetMappingTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f31be1..692b07c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 1.1.0 + +* feat: `PresetTransformer`. + +## 1.0.0 + +* No changes. + ## 0.10.2 * fix: Handle cases where transformed key is different from the original. diff --git a/README.md b/README.md index dc16368f..58224fea 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ a few keystrokes. * Improve non-framework usage. * Warm up proxies on build time from the list of classes provided by the user. * Lazy-loading using Doctrine `Collection` type hint on the target side. -* `PresetTransformer`. Transforms objects using a predetermined table of values. ## Documentation diff --git a/config/services.php b/config/services.php index 937169a5..e07de6c9 100644 --- a/config/services.php +++ b/config/services.php @@ -49,6 +49,7 @@ use Rekalogika\Mapper\Transformer\Implementation\ObjectToArrayTransformer; use Rekalogika\Mapper\Transformer\Implementation\ObjectToObjectTransformer; use Rekalogika\Mapper\Transformer\Implementation\ObjectToStringTransformer; +use Rekalogika\Mapper\Transformer\Implementation\PresetTransformer; use Rekalogika\Mapper\Transformer\Implementation\ScalarToScalarTransformer; use Rekalogika\Mapper\Transformer\Implementation\StringToBackedEnumTransformer; use Rekalogika\Mapper\Transformer\Implementation\SymfonyUidTransformer; @@ -120,7 +121,7 @@ $services ->set(ScalarToScalarTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -350]); + ->tag('rekalogika.mapper.transformer', ['priority' => -300]); $services ->set(ObjectMapperTransformer::class) @@ -130,29 +131,33 @@ service('rekalogika.mapper.object_mapper.table_factory'), service('rekalogika.mapper.object_mapper.resolver'), ]) - ->tag('rekalogika.mapper.transformer', ['priority' => -400]); + ->tag('rekalogika.mapper.transformer', ['priority' => -350]); $services ->set(DateTimeTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -450]); + ->tag('rekalogika.mapper.transformer', ['priority' => -400]); $services ->set(StringToBackedEnumTransformer::class) + ->tag('rekalogika.mapper.transformer', ['priority' => -450]); + + $services + ->set(SymfonyUidTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -500]); $services - ->set(ClassMethodTransformer::class) - ->args([ - service('rekalogika.mapper.sub_mapper.factory'), - ]) + ->set(ObjectToStringTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -550]); $services - ->set(SymfonyUidTransformer::class) + ->set(PresetTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -600]); $services - ->set(ObjectToStringTransformer::class) + ->set(ClassMethodTransformer::class) + ->args([ + service('rekalogika.mapper.sub_mapper.factory'), + ]) ->tag('rekalogika.mapper.transformer', ['priority' => -650]); $services diff --git a/src/Debug/TraceData.php b/src/Debug/TraceData.php index 1a041273..be030de0 100644 --- a/src/Debug/TraceData.php +++ b/src/Debug/TraceData.php @@ -39,6 +39,7 @@ final class TraceData private ?string $callerClass = null; private ?string $callerType = null; private ?string $callerName = null; + private bool $refused = false; /** * @param null|array $possibleTargetTypes @@ -57,7 +58,18 @@ public function __construct( $this->existingTargetType = \get_debug_type($existingTargetValue); } - public function finalizeTime(float $time): void + public function refusedToTransform(): void + { + $this->refused = true; + } + + public function finalize(float $time, mixed $result): void + { + $this->finalizeTime($time); + $this->finalizeResult($result); + } + + private function finalizeTime(float $time): void { if (count($this->nestedTraceData) === 0) { // If this is the last trace data (no nested trace data) @@ -70,7 +82,7 @@ public function finalizeTime(float $time): void } } - public function finalizeResult(mixed $result): void + private function finalizeResult(mixed $result): void { $this->resultType = \get_debug_type($result); } @@ -96,6 +108,14 @@ public function getNestedTraceData(): array return $this->nestedTraceData; } + /** + * @return array + */ + public function getAcceptedNestedTraceData(): array + { + return array_filter($this->nestedTraceData, fn (self $traceData) => !$traceData->isRefused()); + } + public function addNestedTraceData(self $traceData): void { $this->nestedTraceData[] = $traceData; @@ -144,7 +164,6 @@ public function getSelectedTargetTypeHtml(): string return TypeUtil::getTypeStringHtml($this->selectedTargetType); } return 'mixed'; - } public function getResultType(): string @@ -227,4 +246,12 @@ public function getCaller(): ?array 'name' => $this->callerName, ]; } + + /** + * Get the value of refused + */ + public function isRefused(): bool + { + return $this->refused; + } } diff --git a/src/Debug/TraceableTransformer.php b/src/Debug/TraceableTransformer.php index f6bcaffc..5e5d1eda 100644 --- a/src/Debug/TraceableTransformer.php +++ b/src/Debug/TraceableTransformer.php @@ -18,6 +18,7 @@ use Rekalogika\Mapper\MainTransformer\Model\DebugContext; use Rekalogika\Mapper\MainTransformer\Model\Path; use Rekalogika\Mapper\Transformer\AbstractTransformerDecorator; +use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; use Rekalogika\Mapper\Transformer\TransformerInterface; @@ -103,21 +104,27 @@ public function transform( $caller['type'] ?? null ); - // if we are the root transformer, add the trace data to the - // context, and collect it $context = $context->with($traceData); - $this->dataCollector->collectTraceData($traceData); } - $start = microtime(true); - /** @var mixed */ - $result = $this->decorated->transform($source, $target, $sourceType, $targetType, $context); - $time = microtime(true) - $start; + try { + $start = microtime(true); + /** @var mixed */ + $result = $this->decorated->transform($source, $target, $sourceType, $targetType, $context); + $time = microtime(true) - $start; - $traceData->finalizeTime($time); - $traceData->finalizeResult($result); + $traceData->finalize($time, $result); - return $result; + if (!$parentTraceData) { + $this->dataCollector->collectTraceData($traceData); + } + + return $result; + } catch (RefuseToTransformException $e) { + $traceData->refusedToTransform(); + + throw $e; + } } public function getSupportedTransformation(): iterable diff --git a/src/MainTransformer/Implementation/MainTransformer.php b/src/MainTransformer/Implementation/MainTransformer.php index f954384f..8244a5a3 100644 --- a/src/MainTransformer/Implementation/MainTransformer.php +++ b/src/MainTransformer/Implementation/MainTransformer.php @@ -219,6 +219,10 @@ public function transform( context: $context ); } catch (RefuseToTransformException) { + if ($targetTypeForTransformer !== null) { + $objectCache->undoPreCache($source, $targetTypeForTransformer); + } + continue; } diff --git a/src/ObjectCache/ObjectCache.php b/src/ObjectCache/ObjectCache.php index 59e44448..326133af 100644 --- a/src/ObjectCache/ObjectCache.php +++ b/src/ObjectCache/ObjectCache.php @@ -77,6 +77,23 @@ public function preCache(mixed $source, Type $targetType): void $this->preCache->offsetGet($source)?->offsetSet($targetTypeString, true); } + public function undoPreCache(mixed $source, Type $targetType): void + { + if (!is_object($source)) { + return; + } + + if ($this->isBlacklisted($source)) { + return; + } + + $targetTypeString = $this->typeResolver->getTypeString($targetType); + + if (isset($this->preCache[$source][$targetTypeString])) { + unset($this->preCache[$source][$targetTypeString]); + } + } + public function containsTarget(mixed $source, Type $targetType): bool { if (!is_object($source)) { @@ -156,4 +173,12 @@ public function saveTarget( unset($this->preCache[$source][$targetTypeString]); } } + + /** + * @return \WeakMap> + */ + public function getInternalMapping(): \WeakMap + { + return $this->cache; + } } diff --git a/src/Transformer/Context/PresetMapping.php b/src/Transformer/Context/PresetMapping.php new file mode 100644 index 00000000..3fa9e22a --- /dev/null +++ b/src/Transformer/Context/PresetMapping.php @@ -0,0 +1,117 @@ + + * + * 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\Exception\PresetMappingNotFound; +use Rekalogika\Mapper\Transformer\Model\SplObjectStorageWrapper; + +final readonly class PresetMapping +{ + /** + * @var \WeakMap> + */ + private \WeakMap $mappings; + + /** + * @param iterable> $mappings + */ + public function __construct(iterable $mappings) + { + /** + * @var \WeakMap> + */ + $weakMap = new \WeakMap(); + + foreach ($mappings as $source => $classToTargetMapping) { + $classToTargetMappingArray = []; + + foreach ($classToTargetMapping as $class => $target) { + $classToTargetMappingArray[$class] = $target; + } + + $weakMap[$source] = new \ArrayObject($classToTargetMappingArray); + } + + $this->mappings = $weakMap; + } + + public static function fromObjectCache(ObjectCache $objectCache): self + { + $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); + } + } + + return new self($presetMapping); + } + + /** + * @template T of object + * @param object $source + * @param class-string $targetClass + * @return T + * @throws PresetMappingNotFound + */ + public function findResult(object $source, string $targetClass): object + { + $mappings = $this->mappings[$source] ?? null; + + if (null === $mappings) { + throw new PresetMappingNotFound(); + } + + $result = $mappings[$targetClass] ?? null; + + if (null === $result) { + throw new PresetMappingNotFound(); + } + + if (!($result instanceof $targetClass)) { + throw new PresetMappingNotFound(); + } + + return $result; + } +} diff --git a/src/Transformer/Exception/PresetMappingNotFound.php b/src/Transformer/Exception/PresetMappingNotFound.php new file mode 100644 index 00000000..6476b0d8 --- /dev/null +++ b/src/Transformer/Exception/PresetMappingNotFound.php @@ -0,0 +1,20 @@ + + * + * 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\Exception\RuntimeException; + +class PresetMappingNotFound extends RuntimeException +{ +} diff --git a/src/Transformer/Implementation/PresetTransformer.php b/src/Transformer/Implementation/PresetTransformer.php new file mode 100644 index 00000000..c13263f2 --- /dev/null +++ b/src/Transformer/Implementation/PresetTransformer.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\Implementation; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +use Rekalogika\Mapper\Transformer\Context\PresetMapping; +use Rekalogika\Mapper\Transformer\Exception\PresetMappingNotFound; +use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; +use Rekalogika\Mapper\Transformer\TransformerInterface; +use Rekalogika\Mapper\Transformer\TypeMapping; +use Rekalogika\Mapper\Util\TypeCheck; +use Rekalogika\Mapper\Util\TypeFactory; +use Symfony\Component\PropertyInfo\Type; + +final readonly class PresetTransformer implements TransformerInterface +{ + public function transform( + mixed $source, + mixed $target, + ?Type $sourceType, + ?Type $targetType, + Context $context + ): mixed { + $presetMapping = $context(PresetMapping::class); + + if ($presetMapping === null) { + throw new RefuseToTransformException(); + } + + if (!TypeCheck::isObject($targetType)) { + throw new UnexpectedValueException('Target type must be an object type'); + } + + $class = $targetType?->getClassName(); + + if (!is_string($class) || !class_exists($class)) { + throw new UnexpectedValueException('Target type must be a valid class name'); + } + + if (!is_object($source)) { + throw new UnexpectedValueException('Source must be an object'); + } + + try { + return $presetMapping->findResult($source, $class); + } catch (PresetMappingNotFound) { + throw new RefuseToTransformException(); + } + } + + public function getSupportedTransformation(): iterable + { + yield new TypeMapping(TypeFactory::object(), TypeFactory::object(), true); + } +} diff --git a/src/Transformer/TransformerInterface.php b/src/Transformer/TransformerInterface.php index 9b5d6e0e..587154c6 100644 --- a/src/Transformer/TransformerInterface.php +++ b/src/Transformer/TransformerInterface.php @@ -14,12 +14,16 @@ namespace Rekalogika\Mapper\Transformer; use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; use Symfony\Component\PropertyInfo\Type; interface TransformerInterface { public const MIXED = 'mixed'; + /** + * @throws RefuseToTransformException + */ public function transform( mixed $source, mixed $target, diff --git a/templates/data_collector.html.twig b/templates/data_collector.html.twig index 1971f16d..f03ad7ef 100644 --- a/templates/data_collector.html.twig +++ b/templates/data_collector.html.twig @@ -93,7 +93,7 @@ Path Source Type - Existing Target Value Type + Existing Target Instance Type Target Type Hint Selected Target Type Target Instance Type @@ -231,7 +231,7 @@ - {% for child in tracedata.nestedTraceData %} + {% for child in tracedata.acceptedNestedTraceData %} {{ _self.render_row(child, depth + 1) }} {% endfor %} {% endmacro %} diff --git a/tests/IntegrationTest/MappingTest.php b/tests/IntegrationTest/MappingTest.php index eedc74d2..4a1d91a8 100644 --- a/tests/IntegrationTest/MappingTest.php +++ b/tests/IntegrationTest/MappingTest.php @@ -22,6 +22,7 @@ use Rekalogika\Mapper\Transformer\Implementation\CopyTransformer; use Rekalogika\Mapper\Transformer\Implementation\DateTimeTransformer; use Rekalogika\Mapper\Transformer\Implementation\ObjectToStringTransformer; +use Rekalogika\Mapper\Transformer\Implementation\PresetTransformer; use Rekalogika\Mapper\Transformer\Implementation\ScalarToScalarTransformer; use Rekalogika\Mapper\Transformer\Implementation\StringToBackedEnumTransformer; use Rekalogika\Mapper\Transformer\Implementation\TraversableToArrayAccessTransformer; @@ -58,11 +59,18 @@ public function testMapping( $this->assertNotEmpty($searchResult); - $first = $searchResult[0] ?? null; - $this->assertNotNull($first); + $selected = $searchResult[0] ?? null; + + $this->assertNotNull($selected); + + if (\str_contains($selected->getTransformerServiceId(), PresetTransformer::class)) { + $selected = $searchResult[1] ?? null; + } + + $this->assertNotNull($selected); $transformer = $transformerRegistry->get( - $first->getTransformerServiceId() + $selected->getTransformerServiceId() ); $this->assertTransformerInstanceOf( diff --git a/tests/IntegrationTest/PresetMappingTest.php b/tests/IntegrationTest/PresetMappingTest.php new file mode 100644 index 00000000..8db69844 --- /dev/null +++ b/tests/IntegrationTest/PresetMappingTest.php @@ -0,0 +1,51 @@ + + * + * 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\ObjectCache\ObjectCache; +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\TypeResolver\TypeResolverInterface; +use Rekalogika\Mapper\Util\TypeFactory; + +class PresetMappingTest extends FrameworkTestCase +{ + private function createObjectCache(): ObjectCache + { + $typeResolver = $this->get('rekalogika.mapper.type_resolver'); + $this->assertInstanceOf(TypeResolverInterface::class, $typeResolver); + + return new ObjectCache($typeResolver); + } + + public function testFromObjectCache(): void + { + $objectCache = $this->createObjectCache(); + + $source = new ObjectWithScalarProperties(); + $targetType = TypeFactory::objectOfClass(ObjectWithScalarProperties::class); + $target = new ObjectWithScalarPropertiesDto(); + + $objectCache->saveTarget($source, $targetType, $target); + + $presetMapping = PresetMapping::fromObjectCache($objectCache); + + $result = $presetMapping->findResult($target, $source::class); + + $this->assertSame($source, $result); + } + +}