From 85133ba4f1c8be200b950ce614986793882987d8 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:02:34 +0700 Subject: [PATCH] Add a caching layer for `TypeResolver` --- CHANGELOG.md | 1 + config/services.php | 16 ++- src/MainTransformer.php | 21 +++- src/MapperFactory.php | 3 +- src/TypeResolver/CachingTypeResolver.php | 118 +++++++++++++++++++++ src/TypeResolver/TypeResolver.php | 18 +--- src/TypeResolver/TypeResolverInterface.php | 14 ++- src/Util/TypeUtil.php | 10 +- 8 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 src/TypeResolver/CachingTypeResolver.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9beaee..6a5aaa93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Use property info caching in non-framework usage * Add mapping caching * Change console command to use `rekalogika` prefix +* Add a caching layer for `TypeResolver` ## 0.5.2 diff --git a/config/services.php b/config/services.php index c1756a49..3a50e3aa 100644 --- a/config/services.php +++ b/config/services.php @@ -30,6 +30,7 @@ use Rekalogika\Mapper\Transformer\StringToBackedEnumTransformer; use Rekalogika\Mapper\Transformer\TraversableToArrayAccessTransformer; use Rekalogika\Mapper\Transformer\TraversableToTraversableTransformer; +use Rekalogika\Mapper\TypeResolver\CachingTypeResolver; use Rekalogika\Mapper\TypeResolver\TypeResolver; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; @@ -158,15 +159,24 @@ ]) ->tag('kernel.cache_warmer'); + # type resolver + + $services + ->set('rekalogika.mapper.type_resolver', TypeResolver::class); + + $services + ->set('rekalogika.mapper.type_resolver.caching', CachingTypeResolver::class) + ->decorate('rekalogika.mapper.type_resolver') + ->args([ + service('rekalogika.mapper.type_resolver.caching.inner'), + ]); + # other services $services ->set('rekalogika.mapper.object_cache_factory', ObjectCacheFactory::class) ->args([service('rekalogika.mapper.type_resolver')]); - $services - ->set('rekalogika.mapper.type_resolver', TypeResolver::class); - $services ->set('rekalogika.mapper.main_transformer', MainTransformer::class) ->args([ diff --git a/src/MainTransformer.php b/src/MainTransformer.php index e74f215b..aacb67b6 100644 --- a/src/MainTransformer.php +++ b/src/MainTransformer.php @@ -84,12 +84,27 @@ public function transform( $objectCache = $context[self::OBJECT_CACHE]; } - // init vars + // gets simple target types from the provided target type - $targetType = $this->typeResolver->getSimpleTypes($targetType); - $sourceType = $this->typeResolver->guessTypeFromVariable($source); + if ($targetType instanceof Type) { + $targetType = [$targetType]; + } + + $simpleTargetTypes = []; foreach ($targetType as $singleTargetType) { + foreach ($this->typeResolver->getSimpleTypes($singleTargetType) as $simpleType) { + $simpleTargetTypes[] = $simpleType; + } + } + + // guess the source type + + $sourceType = $this->typeResolver->guessTypeFromVariable($source); + + // iterate simple target types and find the suitable transformer + + foreach ($simpleTargetTypes as $singleTargetType) { $transformers = $this->getTransformers($sourceType, $singleTargetType); foreach ($transformers as $transformer) { diff --git a/src/MapperFactory.php b/src/MapperFactory.php index a207a91c..4d55ab2d 100644 --- a/src/MapperFactory.php +++ b/src/MapperFactory.php @@ -33,6 +33,7 @@ use Rekalogika\Mapper\Transformer\StringToBackedEnumTransformer; use Rekalogika\Mapper\Transformer\TraversableToArrayAccessTransformer; use Rekalogika\Mapper\Transformer\TraversableToTraversableTransformer; +use Rekalogika\Mapper\TypeResolver\CachingTypeResolver; use Rekalogika\Mapper\TypeResolver\TypeResolver; use Rekalogika\Mapper\TypeResolver\TypeResolverInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -358,7 +359,7 @@ protected function getTraversableToTraversableTransformer(): TransformerInterfac protected function getTypeResolver(): TypeResolverInterface { if (null === $this->typeResolver) { - $this->typeResolver = new TypeResolver(); + $this->typeResolver = new CachingTypeResolver(new TypeResolver()); } return $this->typeResolver; diff --git a/src/TypeResolver/CachingTypeResolver.php b/src/TypeResolver/CachingTypeResolver.php new file mode 100644 index 00000000..67e1db6f --- /dev/null +++ b/src/TypeResolver/CachingTypeResolver.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TypeResolver; + +use Rekalogika\Mapper\Model\MixedType; +use Symfony\Component\PropertyInfo\Type; + +class CachingTypeResolver implements TypeResolverInterface +{ + public function __construct( + private TypeResolverInterface $decorated, + ) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->typeStringCache = new \WeakMap(); + + /** @psalm-suppress PropertyTypeCoercion */ + $this->simpleTypesCache = new \WeakMap(); + + /** @psalm-suppress PropertyTypeCoercion */ + $this->isSimpleTypeCache = new \WeakMap(); + } + + // cheap. so we don't cache + + public function guessTypeFromVariable(mixed $variable): Type + { + return $this->decorated->guessTypeFromVariable($variable); + } + + // can be expensive in a loop. we cache using a weakmap + + /** + * @var \WeakMap + */ + private \WeakMap $typeStringCache; + + public function getTypeString(Type|MixedType $type): string + { + if ($result = $this->typeStringCache[$type] ?? null) { + return $result; + } + + $typeString = $this->decorated->getTypeString($type); + $this->typeStringCache->offsetSet($type, $typeString); + + return $typeString; + } + + // can be expensive in a loop. we cache using a weakmap + + /** + * @var \WeakMap> + */ + private \WeakMap $simpleTypesCache; + + public function getSimpleTypes(Type $type): array + { + if ($result = $this->simpleTypesCache[$type] ?? null) { + return $result; + } + + $simpleTypes = $this->decorated->getSimpleTypes($type); + $this->simpleTypesCache->offsetSet($type, $simpleTypes); + + return $simpleTypes; + } + + // can be expensive in a loop. we cache using a weakmap + + /** + * @var \WeakMap + */ + private \WeakMap $isSimpleTypeCache; + + public function isSimpleType(Type $type): bool + { + if ($result = $this->isSimpleTypeCache[$type] ?? null) { + return $result; + } + + $isSimpleType = $this->decorated->isSimpleType($type); + $this->isSimpleTypeCache->offsetSet($type, $isSimpleType); + + return $isSimpleType; + } + + // expensive, but impossible to cache using a weakmap, so we use an array + + /** + * @var array> + */ + private array $applicableTypeStringsCache = []; + + public function getApplicableTypeStrings(Type|MixedType $type): array + { + $typeString = $this->getTypeString($type); + + if (isset($this->applicableTypeStringsCache[$typeString])) { + return $this->applicableTypeStringsCache[$typeString]; + } + + $applicableTypeStrings = $this->decorated->getApplicableTypeStrings($type); + $this->applicableTypeStringsCache[$typeString] = $applicableTypeStrings; + + return $applicableTypeStrings; + } +} diff --git a/src/TypeResolver/TypeResolver.php b/src/TypeResolver/TypeResolver.php index b691fdef..03cece48 100644 --- a/src/TypeResolver/TypeResolver.php +++ b/src/TypeResolver/TypeResolver.php @@ -74,31 +74,21 @@ public function isSimpleType(Type $type): bool /** * Gets all the possible simple types from a Type * - * @param Type|array $type + * @param Type $type * @return array */ - public function getSimpleTypes(Type|array $type): array + public function getSimpleTypes(Type $type): array { return TypeUtil::getSimpleTypes($type); } - public function getApplicableTypeStrings(array|Type|MixedType $type): array + public function getApplicableTypeStrings(Type|MixedType $type): array { if ($type instanceof MixedType) { $type = ['mixed']; return $type; } - if ($type instanceof Type) { - $type = [$type]; - } - - $typeStrings = []; - - foreach ($type as $type) { - $typeStrings = array_merge($typeStrings, TypeUtil::getAllTypeStrings($type, true)); - } - - return $typeStrings; + return TypeUtil::getAllTypeStrings($type, true); } } diff --git a/src/TypeResolver/TypeResolverInterface.php b/src/TypeResolver/TypeResolverInterface.php index 1685ab48..2695c516 100644 --- a/src/TypeResolver/TypeResolverInterface.php +++ b/src/TypeResolver/TypeResolverInterface.php @@ -23,15 +23,21 @@ interface TypeResolverInterface */ public function guessTypeFromVariable(mixed $variable): Type; + /** + * Gets the string representation of a Type. + * + * @param Type|MixedType $type + * @return string + */ public function getTypeString(Type|MixedType $type): string; /** * Gets all the possible simple types from a Type * - * @param Type|array $type + * @param Type $type * @return array */ - public function getSimpleTypes(Type|array $type): array; + public function getSimpleTypes(Type $type): array; /** * Simple Type is a type that is not nullable, and does not have more @@ -49,8 +55,8 @@ public function isSimpleType(Type $type): bool; * * Note: IteratorAggregate extends Traversable * - * @param array|Type|MixedType $type + * @param Type|MixedType $type * @return array */ - public function getApplicableTypeStrings(array|Type|MixedType $type): array; + public function getApplicableTypeStrings(Type|MixedType $type): array; } diff --git a/src/Util/TypeUtil.php b/src/Util/TypeUtil.php index f9114b49..2d3da419 100644 --- a/src/Util/TypeUtil.php +++ b/src/Util/TypeUtil.php @@ -67,17 +67,13 @@ public static function isSimpleType(Type $type): bool /** * Gets all the possible simple types from a Type * - * @param Type|array $type + * @param Type $type * @return array */ #[Friend(TypeResolver::class)] - public static function getSimpleTypes(Type|array $type, bool $withParents = false): array + public static function getSimpleTypes(Type $type, bool $withParents = false): array { - if (!is_array($type)) { - $type = [$type]; - } - - return self::getTypePermutations($type, withParents: $withParents); + return self::getTypePermutations([$type], withParents: $withParents); } /**