├── .gitattributes ├── .github ├── release.yml ├── dependabot.yml └── workflows │ └── php.yml ├── .gitignore ├── .phive └── phars.xml ├── templates └── map.svg ├── .editorconfig ├── .env ├── src ├── Exception │ ├── ExceptionInterface.php │ ├── RefuseToMapException.php │ ├── BadMethodCallException.php │ ├── RuntimeException.php │ ├── InvalidArgumentException.php │ ├── LogicException.php │ ├── UnexpectedValueException.php │ ├── InvalidClassException.php │ └── ContextAwareExceptionTrait.php ├── Attribute │ ├── Eager.php │ ├── AsObjectMapper.php │ ├── AllowDelete.php │ ├── AllowTargetDelete.php │ ├── MapperAttributeInterface.php │ ├── Unalterable.php │ ├── AsPropertyMapper.php │ ├── Map.php │ ├── InheritanceMap.php │ └── DateTimeOptions.php ├── ObjectCache │ ├── Sentinel │ │ ├── AbstractObjectCacheSentinel.php │ │ └── CachedTargetObjectNotFoundSentinel.php │ ├── ObjectCacheFactoryInterface.php │ ├── Exception │ │ ├── CircularReferenceException.php │ │ └── NonSimpleTypeException.php │ └── Implementation │ │ └── ObjectCacheFactory.php ├── Transformer │ ├── Exception │ │ ├── MissingMemberTypeException.php │ │ ├── PresetMappingNotFound.php │ │ ├── NotMappableValueException.php │ │ ├── UnsupportedPropertyMappingException.php │ │ ├── RefuseToTransformException.php │ │ ├── UninitializedSourcePropertyException.php │ │ ├── NotAClassException.php │ │ ├── PropertyPathAwarePropertyInfoExtractorException.php │ │ ├── ClassNotInstantiableException.php │ │ ├── InvalidClassException.php │ │ ├── InvalidTypeInArgumentException.php │ │ ├── InternalClassUnsupportedException.php │ │ ├── UnableToReadException.php │ │ ├── UnableToWriteException.php │ │ ├── ExtraTargetPropertyNotFoundException.php │ │ ├── SourceClassNotInInheritanceMapException.php │ │ ├── NullSourceButMandatoryTargetException.php │ │ ├── PairedPropertyNotFoundException.php │ │ ├── MissingMemberValueTypeException.php │ │ └── MissingMemberKeyTypeException.php │ ├── ObjectToObjectMetadata │ │ ├── Visibility.php │ │ ├── ReadMode.php │ │ ├── WriteMode.php │ │ ├── ObjectToObjectMetadataFactoryInterface.php │ │ └── Implementation │ │ │ └── ProxyResolvingObjectToObjectMetadataFactory.php │ ├── Context │ │ ├── SourceClassAttributes.php │ │ ├── TargetClassAttributes.php │ │ ├── SourcePropertyAttributes.php │ │ └── TargetPropertyAttributes.php │ ├── MainTransformerAwareInterface.php │ ├── MetadataUtil │ │ ├── DynamicPropertiesDeterminerInterface.php │ │ ├── Model │ │ │ ├── Attributes.php │ │ │ └── ClassMetadata.php │ │ ├── PropertyMetadataFactoryInterface.php │ │ ├── ClassMetadataFactoryInterface.php │ │ ├── UnalterableDeterminerInterface.php │ │ ├── PropertyMappingResolverInterface.php │ │ ├── AttributesExtractorInterface.php │ │ ├── DynamicPropertiesDeterminer │ │ │ ├── DynamicPropertiesDeterminer.php │ │ │ └── CachingDynamicPropertiesDeterminer.php │ │ ├── PropertyAccessInfoExtractorInterface.php │ │ ├── PropertyMetadataFactory │ │ │ └── Util.php │ │ ├── TargetClassResolverInterface.php │ │ ├── AttributesExtractor │ │ │ └── CachingAttributesExtractor.php │ │ └── TargetClassResolver │ │ │ └── CachingTargetClassResolver.php │ ├── ArrayLikeMetadata │ │ └── ArrayLikeMetadataFactoryInterface.php │ ├── Processor │ │ ├── ObjectProcessorInterface.php │ │ ├── ObjectProcessorFactoryInterface.php │ │ └── ObjectProcessor │ │ │ └── DefaultObjectProcessorFactory.php │ ├── MixedType.php │ ├── EagerPropertiesResolver │ │ ├── EagerPropertiesResolverInterface.php │ │ └── Implementation │ │ │ ├── ChainEagerPropertiesResolver.php │ │ │ ├── DoctrineEagerPropertiesResolver.php │ │ │ └── HeuristicsEagerPropertiesResolver.php │ ├── TransformerInterface.php │ ├── MainTransformerAwareTrait.php │ ├── Implementation │ │ ├── NullToNullTransformer.php │ │ ├── CopyTransformer.php │ │ ├── PresetTransformer.php │ │ └── ScalarToScalarTransformer.php │ ├── AbstractTransformerDecorator.php │ ├── Model │ │ ├── TraversableCountableWrapper.php │ │ └── SplObjectStorageWrapper.php │ ├── TypeMapping.php │ └── Trait │ │ └── WarmableArrayLikeTransformerTrait.php ├── CacheWarmer │ ├── WarmableProxyRegistryInterface.php │ ├── WarmableProxyFactoryInterface.php │ ├── WarmableMapperInterface.php │ ├── WarmableCacheInterface.php │ ├── WarmableArrayLikeMetadataFactoryInterface.php │ ├── WarmableTransformerInterface.php │ ├── WarmableMainTransformerInterface.php │ ├── WarmableObjectToObjectMetadataFactoryInterface.php │ ├── WarmableTransformerRegistryInterface.php │ ├── WarmableObjectMapperResolverInterface.php │ ├── MappingCollection.php │ ├── MappingCache.php │ └── MapperCacheWarmer.php ├── Proxy │ ├── ProxyRegistryInterface.php │ ├── ProxyAutoloaderInterface.php │ ├── ProxyMetadataFactoryInterface.php │ ├── ProxyGeneratorInterface.php │ ├── ProxyNamer.php │ ├── ProxyFactoryInterface.php │ ├── Exception │ │ └── ProxyNotSupportedException.php │ ├── Metadata │ │ └── PropertyMetadata.php │ └── Implementation │ │ ├── CachingProxyMetadataFactory.php │ │ ├── DoctrineProxyFactory.php │ │ ├── DynamicPropertiesProxyFactory.php │ │ ├── ProxyFactory.php │ │ ├── WarmableVarExporterProxyFactory.php │ │ ├── PhpProxyFactory.php │ │ ├── ProxyGenerator.php │ │ └── VarExporterProxyFactory.php ├── Mapping │ ├── MappingFactoryInterface.php │ ├── Implementation │ │ └── MappingCacheWarmer.php │ └── Mapping.php ├── CustomMapper │ ├── ObjectMapperTableFactoryInterface.php │ ├── PropertyMapperResolverInterface.php │ ├── Exception │ │ └── ObjectMapperNotFoundException.php │ ├── ObjectMapperResolverInterface.php │ ├── ObjectMapperTableEntry.php │ └── Implementation │ │ ├── ObjectMapperTableWarmer.php │ │ ├── ObjectMapperTableFactory.php │ │ └── ObjectMapperResolver.php ├── ListInterface.php ├── CollectionInterface.php ├── MapperInterface.php ├── Command │ └── MarkdownLikeTableStyle.php ├── IterableMapperInterface.php ├── SubMapper │ ├── Exception │ │ └── CacheNotSupportedException.php │ ├── SubMapperFactoryInterface.php │ ├── SubMapperInterface.php │ └── Implementation │ │ └── SubMapperFactory.php ├── Debug │ ├── Helper.php │ ├── TraceableMappingFactory.php │ ├── TraceableObjectToObjectMetadataFactory.php │ └── TraceableMapper.php ├── MainTransformer │ ├── MainTransformerInterface.php │ ├── Model │ │ ├── DebugContext.php │ │ └── Path.php │ └── Exception │ │ ├── CircularReferenceException.php │ │ ├── CannotFindTransformerException.php │ │ └── TransformerReturnsUnexpectedValueException.php ├── TransformerRegistry │ ├── TransformerRegistryInterface.php │ ├── SearchResult.php │ └── SearchResultEntry.php ├── Context │ ├── MapperOptions.php │ ├── ExtraTargetValues.php │ └── Context.php ├── Util │ ├── ServiceLocator.php │ └── TypeGuesser.php ├── DependencyInjection │ └── CompilerPass │ │ └── RemoveOptionalDefinitionPass.php ├── ServiceMethod │ └── ServiceMethodSpecification.php ├── RekalogikaMapperBundle.php └── TypeResolver │ ├── Implementation │ └── TypeResolver.php │ └── TypeResolverInterface.php ├── phpstan-extension.neon ├── config ├── debug.php └── non-debug.php ├── phpunit.xml.dist.bak ├── LICENSE ├── .php-cs-fixer.dist.php ├── phpunit.xml.dist ├── Makefile └── phpstan.neon.dist /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | labels: 3 | - "*" 4 | exclude: 5 | labels: 6 | - dependencies 7 | - minor 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | .phpunit.cache 4 | tools 5 | .php-cs-fixer.cache 6 | var 7 | rector.log 8 | .vscode 9 | .env.local 10 | -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /templates/map.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | charset = utf-8 7 | end_of_line = LF 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{php,html,twig}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.md] 16 | max_line_length = 80 17 | 18 | [COMMIT_EDITMSG] 19 | max_line_length = 0 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | APP_ENV=test 2 | PHP=php 3 | SYMFONY=symfony 4 | COMPOSER=composer 5 | 6 | # use .env.local to override, example: 7 | 8 | # PHP with xdebug: 9 | # PHP="php -dxdebug.start_with_request=yes -dxdebug.mode=debug" 10 | 11 | # PHP using docker and xdebug: 12 | #PHP="docker run -it --rm --user $$(id -u):$$(id -g) -v $$PWD:/usr/src/myapp -w /usr/src/myapp php:8.4.0beta5-cli php -dxdebug.start_with_request=yes -dxdebug.mode=debug" 13 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | interface ExceptionInterface extends \Throwable {} 17 | -------------------------------------------------------------------------------- /src/Attribute/Eager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS)] 17 | final readonly class Eager {} 18 | -------------------------------------------------------------------------------- /src/ObjectCache/Sentinel/AbstractObjectCacheSentinel.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache\Sentinel; 15 | 16 | abstract class AbstractObjectCacheSentinel {} 17 | -------------------------------------------------------------------------------- /src/Attribute/AsObjectMapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_METHOD)] 17 | final readonly class AsObjectMapper {} 18 | -------------------------------------------------------------------------------- /src/Exception/RefuseToMapException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | final class RefuseToMapException extends \RuntimeException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /src/Attribute/AllowDelete.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)] 17 | final readonly class AllowDelete {} 18 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | final class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface {} 17 | -------------------------------------------------------------------------------- /src/Transformer/Exception/MissingMemberTypeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | abstract class MissingMemberTypeException extends NotMappableValueException {} 17 | -------------------------------------------------------------------------------- /src/Attribute/AllowTargetDelete.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)] 17 | final readonly class AllowTargetDelete {} 18 | -------------------------------------------------------------------------------- /src/ObjectCache/ObjectCacheFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache; 15 | 16 | interface ObjectCacheFactoryInterface 17 | { 18 | public function createObjectCache(): ObjectCache; 19 | } 20 | -------------------------------------------------------------------------------- /src/ObjectCache/Sentinel/CachedTargetObjectNotFoundSentinel.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache\Sentinel; 15 | 16 | final class CachedTargetObjectNotFoundSentinel extends AbstractObjectCacheSentinel {} 17 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | class RuntimeException extends \RuntimeException implements ExceptionInterface 17 | { 18 | use ContextAwareExceptionTrait; 19 | } 20 | -------------------------------------------------------------------------------- /src/Transformer/Exception/PresetMappingNotFound.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\RuntimeException; 17 | 18 | final class PresetMappingNotFound extends RuntimeException {} 19 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 17 | { 18 | use ContextAwareExceptionTrait; 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | /** 17 | * @api 18 | */ 19 | class LogicException extends \LogicException implements ExceptionInterface 20 | { 21 | use ContextAwareExceptionTrait; 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface 17 | { 18 | use ContextAwareExceptionTrait; 19 | } 20 | -------------------------------------------------------------------------------- /phpstan-extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | rekalogika-mapper: 3 | mapperDumpFile: null 4 | 5 | parametersSchema: 6 | rekalogika-mapper: structure([ 7 | mapperDumpFile: schema(string(), nullable()) 8 | ]) 9 | 10 | services: 11 | - 12 | class: Rekalogika\Mapper\PHPStan\MapperCollector 13 | tags: 14 | - phpstan.collector 15 | - 16 | class: Rekalogika\Mapper\PHPStan\MapperRule 17 | arguments: 18 | mapperDumpFile: %rekalogika-mapper.mapperDumpFile% 19 | tags: 20 | - phpstan.rules.rule 21 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableProxyRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | interface WarmableProxyRegistryInterface 17 | { 18 | public function warmingRegisterProxy(string $class, string $sourceCode): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/Proxy/ProxyRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface ProxyRegistryInterface 20 | { 21 | public function registerProxy(string $class, string $sourceCode): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Attribute/MapperAttributeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | /** 17 | * To allow matching using an attribute, your attribute class must implement 18 | * this interface. 19 | */ 20 | interface MapperAttributeInterface {} 21 | -------------------------------------------------------------------------------- /src/Transformer/Exception/NotMappableValueException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 17 | 18 | abstract class NotMappableValueException extends UnexpectedValueException {} 19 | -------------------------------------------------------------------------------- /src/Mapping/MappingFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Mapping; 15 | 16 | /** 17 | * Initialize transformer mappings 18 | * 19 | * @internal 20 | */ 21 | interface MappingFactoryInterface 22 | { 23 | public function getMapping(): Mapping; 24 | } 25 | -------------------------------------------------------------------------------- /src/Attribute/Unalterable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS)] 17 | final readonly class Unalterable 18 | { 19 | public function __construct( 20 | public bool $isUnalterable = true, 21 | ) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/CustomMapper/ObjectMapperTableFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface ObjectMapperTableFactoryInterface 20 | { 21 | public function createObjectMapperTable(): ObjectMapperTable; 22 | } 23 | -------------------------------------------------------------------------------- /src/Proxy/ProxyAutoloaderInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface ProxyAutoloaderInterface 20 | { 21 | public function registerAutoloader(): void; 22 | 23 | public function unregisterAutoloader(): void; 24 | } 25 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableProxyFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | interface WarmableProxyFactoryInterface 17 | { 18 | /** 19 | * @param class-string $class 20 | */ 21 | public function warmingCreateProxy(string $class): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformer/ObjectToObjectMetadata/Visibility.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata; 15 | 16 | /** 17 | * @internal 18 | */ 19 | enum Visibility 20 | { 21 | case None; 22 | case Public; 23 | case Protected; 24 | case Private; 25 | } 26 | -------------------------------------------------------------------------------- /src/Transformer/Context/SourceClassAttributes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Context; 15 | 16 | /** 17 | * @implements \IteratorAggregate 18 | */ 19 | final readonly class SourceClassAttributes implements \IteratorAggregate 20 | { 21 | use AttributesTrait; 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformer/Context/TargetClassAttributes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Context; 15 | 16 | /** 17 | * @implements \IteratorAggregate 18 | */ 19 | final readonly class TargetClassAttributes implements \IteratorAggregate 20 | { 21 | use AttributesTrait; 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformer/Context/SourcePropertyAttributes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Context; 15 | 16 | /** 17 | * @implements \IteratorAggregate 18 | */ 19 | final readonly class SourcePropertyAttributes implements \IteratorAggregate 20 | { 21 | use AttributesTrait; 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformer/Context/TargetPropertyAttributes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Context; 15 | 16 | /** 17 | * @implements \IteratorAggregate 18 | */ 19 | final readonly class TargetPropertyAttributes implements \IteratorAggregate 20 | { 21 | use AttributesTrait; 22 | } 23 | -------------------------------------------------------------------------------- /src/Transformer/Exception/UnsupportedPropertyMappingException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\RuntimeException; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class UnsupportedPropertyMappingException extends RuntimeException {} 22 | -------------------------------------------------------------------------------- /src/ListInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper; 15 | 16 | /** 17 | * @template TKey of int 18 | * @template TValue 19 | * @extends \IteratorAggregate 20 | * @extends \ArrayAccess 21 | */ 22 | interface ListInterface extends \ArrayAccess, \IteratorAggregate, \Countable {} 23 | -------------------------------------------------------------------------------- /src/Transformer/ObjectToObjectMetadata/ReadMode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata; 15 | 16 | /** 17 | * @internal 18 | */ 19 | enum ReadMode 20 | { 21 | case None; 22 | case Method; 23 | case Property; 24 | case DynamicProperty; 25 | case PropertyPath; 26 | } 27 | -------------------------------------------------------------------------------- /src/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper; 15 | 16 | /** 17 | * @template TKey of array-key 18 | * @template TValue 19 | * @extends \IteratorAggregate 20 | * @extends \ArrayAccess 21 | */ 22 | interface CollectionInterface extends \ArrayAccess, \IteratorAggregate, \Countable {} 23 | -------------------------------------------------------------------------------- /src/Transformer/MainTransformerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; 17 | 18 | interface MainTransformerAwareInterface 19 | { 20 | public function withMainTransformer(MainTransformerInterface $mainTransformer): static; 21 | } 22 | -------------------------------------------------------------------------------- /src/ObjectCache/Exception/CircularReferenceException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\ExceptionInterface; 17 | use Rekalogika\Mapper\Exception\RuntimeException; 18 | 19 | final class CircularReferenceException extends RuntimeException implements ExceptionInterface {} 20 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/DynamicPropertiesDeterminerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface DynamicPropertiesDeterminerInterface 20 | { 21 | /** 22 | * @param class-string $class 23 | */ 24 | public function allowsDynamicProperties(string $class): bool; 25 | } 26 | -------------------------------------------------------------------------------- /src/Transformer/ObjectToObjectMetadata/WriteMode.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata; 15 | 16 | /** 17 | * @internal 18 | */ 19 | enum WriteMode 20 | { 21 | case None; 22 | case Method; 23 | case Property; 24 | case AdderRemover; 25 | case Constructor; 26 | case DynamicProperty; 27 | case PropertyPath; 28 | } 29 | -------------------------------------------------------------------------------- /src/Proxy/ProxyMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | use Rekalogika\Mapper\Proxy\Metadata\ClassMetadata; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface ProxyMetadataFactoryInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function getMetadata(string $class): ClassMetadata; 27 | } 28 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableMapperInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | interface WarmableMapperInterface 17 | { 18 | /** 19 | * @param class-string $sourceClass 20 | * @param class-string $targetClass 21 | */ 22 | public function warmingMap( 23 | string $sourceClass, 24 | string $targetClass, 25 | ): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/MapperInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | interface MapperInterface 19 | { 20 | /** 21 | * @template T of object 22 | * @param class-string|T $target 23 | * @return T 24 | */ 25 | public function map(object $source, object|string $target, ?Context $context = null): object; 26 | } 27 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/Model/Attributes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\Model; 15 | 16 | use Rekalogika\Mapper\Transformer\Context\AttributesTrait; 17 | 18 | /** 19 | * @implements \IteratorAggregate 20 | * @internal 21 | */ 22 | final readonly class Attributes implements \IteratorAggregate 23 | { 24 | use AttributesTrait; 25 | } 26 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableCacheInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Psr\Cache\CacheItemInterface; 17 | use Psr\Cache\CacheItemPoolInterface; 18 | 19 | interface WarmableCacheInterface extends CacheItemPoolInterface 20 | { 21 | public function getWarmedUpItem(string $key): CacheItemInterface; 22 | 23 | public function saveWarmedUp(CacheItemInterface $item): bool; 24 | } 25 | -------------------------------------------------------------------------------- /src/Command/MarkdownLikeTableStyle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Command; 15 | 16 | use Symfony\Component\Console\Helper\TableStyle; 17 | 18 | /** 19 | * Markdown-like table style, for the ease of copy-paste to documentation. 20 | */ 21 | final class MarkdownLikeTableStyle extends TableStyle 22 | { 23 | public function __construct() 24 | { 25 | $this->setDefaultCrossingChar('|'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformer/ArrayLikeMetadata/ArrayLikeMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ArrayLikeMetadata; 15 | 16 | use Symfony\Component\PropertyInfo\Type; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface ArrayLikeMetadataFactoryInterface 22 | { 23 | public function createArrayLikeMetadata( 24 | Type $sourceType, 25 | Type $targetType, 26 | ): ArrayLikeMetadata; 27 | } 28 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableArrayLikeMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\Transformer\ArrayLikeMetadata\ArrayLikeMetadata; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | interface WarmableArrayLikeMetadataFactoryInterface 20 | { 21 | public function warmingCreateArrayLikeMetadata( 22 | Type $sourceType, 23 | Type $targetType, 24 | ): ArrayLikeMetadata; 25 | } 26 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | interface WarmableTransformerInterface 20 | { 21 | public function warmingTransform( 22 | Type $sourceType, 23 | Type $targetType, 24 | Context $context, 25 | ): void; 26 | 27 | public function isWarmable(): bool; 28 | } 29 | -------------------------------------------------------------------------------- /src/IterableMapperInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | interface IterableMapperInterface 19 | { 20 | /** 21 | * @template T of object 22 | * @param iterable $source 23 | * @param class-string $target 24 | * @return iterable 25 | */ 26 | public function mapIterable(iterable $source, string $target, ?Context $context = null): iterable; 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformer/Processor/ObjectProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Processor; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * @internal 21 | */ 22 | interface ObjectProcessorInterface 23 | { 24 | public function transform( 25 | object $source, 26 | ?object $target, 27 | Type $targetType, 28 | Context $context, 29 | ): object; 30 | } 31 | -------------------------------------------------------------------------------- /src/Attribute/AsPropertyMapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 17 | final readonly class AsPropertyMapper 18 | { 19 | /** 20 | * @param class-string|null $targetClass 21 | */ 22 | public function __construct( 23 | public ?string $property = null, 24 | public ?string $targetClass = null, 25 | public bool $ignoreUninitialized = false, 26 | ) {} 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformer/Exception/RefuseToTransformException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\RuntimeException; 17 | 18 | /** 19 | * If a transformer throw this exception, it means that the transformer is not 20 | * able to handle the given source type in an ad-hoc basis. The main transformer 21 | * will try the next transformer for the task. 22 | */ 23 | final class RefuseToTransformException extends RuntimeException {} 24 | -------------------------------------------------------------------------------- /src/Proxy/ProxyGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | use Rekalogika\Mapper\Proxy\Exception\ProxyNotSupportedException; 17 | 18 | /** 19 | * @internal Implement ProxyFactoryInterface instead 20 | */ 21 | interface ProxyGeneratorInterface 22 | { 23 | /** 24 | * @param class-string $realClass 25 | * @throws ProxyNotSupportedException 26 | */ 27 | public function generateProxyCode(string $realClass, string $proxyClass): string; 28 | } 29 | -------------------------------------------------------------------------------- /src/Proxy/ProxyNamer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final readonly class ProxyNamer 20 | { 21 | private function __construct() {} 22 | 23 | /** 24 | * @param class-string $class 25 | */ 26 | public static function generateProxyClassName(string $class): string 27 | { 28 | return \sprintf( 29 | 'Rekalogika\Mapper\Generated\__CG__\%s', 30 | $class, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/PropertyMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\Model\PropertyMetadata; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface PropertyMetadataFactoryInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function createPropertyMetadata( 27 | string $class, 28 | string $property, 29 | ): PropertyMetadata; 30 | } 31 | -------------------------------------------------------------------------------- /src/Transformer/Exception/UninitializedSourcePropertyException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\RuntimeException; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class UninitializedSourcePropertyException extends RuntimeException 22 | { 23 | public function __construct(private readonly string $propertyName) {} 24 | 25 | public function getPropertyName(): string 26 | { 27 | return $this->propertyName; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/ClassMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\Model\ClassMetadata; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface ClassMetadataFactoryInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | * @todo collect property path attributes 26 | */ 27 | public function createClassMetadata( 28 | string $class, 29 | ): ClassMetadata; 30 | } 31 | -------------------------------------------------------------------------------- /src/Transformer/MixedType.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | /** 17 | * Sentinel class to indicate mixed type 18 | */ 19 | 20 | final class MixedType 21 | { 22 | private static ?self $instance = null; 23 | 24 | private function __construct() {} 25 | 26 | public static function instance(): self 27 | { 28 | if (self::$instance === null) { 29 | self::$instance = new self(); 30 | } 31 | 32 | return self::$instance; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface ObjectToObjectMetadataFactoryInterface 20 | { 21 | /** 22 | * @param class-string $sourceClass 23 | * @param class-string $targetClass 24 | */ 25 | public function createObjectToObjectMetadata( 26 | string $sourceClass, 27 | string $targetClass, 28 | ): ObjectToObjectMetadata; 29 | } 30 | -------------------------------------------------------------------------------- /src/SubMapper/Exception/CacheNotSupportedException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\SubMapper\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\RuntimeException; 18 | 19 | final class CacheNotSupportedException extends RuntimeException 20 | { 21 | public function __construct(Context $context) 22 | { 23 | parent::__construct('The "cache()" method is not supported, and should be unnecessary in a sub-mapper under a property mapper.', context: $context); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/UnalterableDeterminerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | use Symfony\Component\PropertyInfo\Type; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface UnalterableDeterminerInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function isClassUnalterable(string $class): bool; 27 | 28 | /** 29 | * @param list $types 30 | */ 31 | public function isTypesUnalterable(array $types): bool; 32 | } 33 | -------------------------------------------------------------------------------- /src/Proxy/ProxyFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface ProxyFactoryInterface 20 | { 21 | /** 22 | * @template T of object 23 | * @param class-string $class 24 | * @param callable(T):void $initializer 25 | * @param list $eagerProperties 26 | * @return T 27 | */ 28 | public function createProxy( 29 | string $class, 30 | $initializer, 31 | array $eagerProperties = [], 32 | ): object; 33 | } 34 | -------------------------------------------------------------------------------- /src/Transformer/Processor/ObjectProcessorFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Processor; 15 | 16 | use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; 17 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 18 | 19 | /** 20 | * @internal 21 | */ 22 | interface ObjectProcessorFactoryInterface extends MainTransformerAwareInterface 23 | { 24 | public function getObjectProcessor( 25 | ObjectToObjectMetadata $metadata, 26 | ): ObjectProcessorInterface; 27 | } 28 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableMainTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | interface WarmableMainTransformerInterface 20 | { 21 | /** 22 | * @param array $sourceTypes 23 | * @param array $targetTypes 24 | */ 25 | public function warmingTransform( 26 | array $sourceTypes, 27 | array $targetTypes, 28 | Context $context, 29 | ): void; 30 | } 31 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableObjectToObjectMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 17 | 18 | interface WarmableObjectToObjectMetadataFactoryInterface 19 | { 20 | /** 21 | * @param class-string $sourceClass 22 | * @param class-string $targetClass 23 | */ 24 | public function warmingCreateObjectToObjectMetadata( 25 | string $sourceClass, 26 | string $targetClass, 27 | ): ObjectToObjectMetadata; 28 | } 29 | -------------------------------------------------------------------------------- /src/SubMapper/SubMapperFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\SubMapper; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; 18 | use Symfony\Component\PropertyInfo\Type; 19 | 20 | /** 21 | * @internal 22 | */ 23 | interface SubMapperFactoryInterface 24 | { 25 | public function createSubMapper( 26 | MainTransformerInterface $mainTransformer, 27 | mixed $source, 28 | ?Type $targetType, 29 | Context $context, 30 | ): SubMapperInterface; 31 | } 32 | -------------------------------------------------------------------------------- /src/CustomMapper/PropertyMapperResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper; 15 | 16 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface PropertyMapperResolverInterface 22 | { 23 | /** 24 | * @param class-string $sourceClass 25 | * @param class-string $targetClass 26 | */ 27 | public function getPropertyMapper( 28 | string $sourceClass, 29 | string $targetClass, 30 | string $property, 31 | ): ?ServiceMethodSpecification; 32 | } 33 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/PropertyMappingResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | /** 17 | * @internal 18 | */ 19 | interface PropertyMappingResolverInterface 20 | { 21 | /** 22 | * @param class-string $sourceClass 23 | * @param class-string $targetClass 24 | * @return list 25 | */ 26 | public function getPropertiesToMap( 27 | string $sourceClass, 28 | string $targetClass, 29 | bool $targetAllowsDynamicProperties, 30 | ): array; 31 | } 32 | -------------------------------------------------------------------------------- /config/debug.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | use Rekalogika\Mapper\Debug\MapperDataCollector; 15 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 16 | 17 | return static function (ContainerConfigurator $containerConfigurator): void { 18 | $services = $containerConfigurator->services(); 19 | 20 | $services 21 | ->set('rekalogika.mapper.data_collector', MapperDataCollector::class) 22 | ->tag('data_collector', [ 23 | 'id' => 'rekalogika_mapper', 24 | ]) 25 | ->tag('kernel.reset', ['method' => 'reset']); 26 | }; 27 | -------------------------------------------------------------------------------- /src/Debug/Helper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Debug; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Rekalogika\Mapper\Util\TypeUtil; 18 | use Symfony\Component\PropertyInfo\Type; 19 | 20 | final readonly class Helper 21 | { 22 | /** 23 | * @param Type|array $type 24 | */ 25 | public function typeToHtml(Type|MixedType|array|null $type): string 26 | { 27 | if ($type === null) { 28 | return 'mixed'; 29 | } 30 | 31 | return TypeUtil::getTypeStringHtml($type); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/AttributesExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\Model\Attributes; 17 | 18 | /** 19 | * @internal 20 | */ 21 | interface AttributesExtractorInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function getClassAttributes(string $class): Attributes; 27 | 28 | /** 29 | * @param class-string $class 30 | */ 31 | public function getPropertyAttributes(string $class, string $property): Attributes; 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/InvalidClassException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | final class InvalidClassException extends InvalidArgumentException 17 | { 18 | public function __construct( 19 | private readonly string $class, 20 | int $code = 0, 21 | ?\Throwable $previous = null, 22 | ) { 23 | $message = \sprintf('Class "%s" does not exist.', $class); 24 | 25 | parent::__construct($message, $code, $previous); 26 | } 27 | 28 | public function getClass(): string 29 | { 30 | return $this->class; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Transformer/EagerPropertiesResolver/EagerPropertiesResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\EagerPropertiesResolver; 15 | 16 | interface EagerPropertiesResolverInterface 17 | { 18 | /** 19 | * Takes the source class name, and determine which properties that can be 20 | * read without causing a full hydration of the source. Usually, the 21 | * object's identifier is eager. 22 | * 23 | * @param class-string $sourceClass 24 | * @return list 25 | */ 26 | public function getEagerProperties(string $sourceClass): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformer/Exception/NotAClassException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | final class NotAClassException extends NotMappableValueException 19 | { 20 | public function __construct( 21 | string $class, 22 | ?Context $context = null, 23 | ) { 24 | parent::__construct( 25 | message: \sprintf( 26 | 'Trying to map to "%s", but it is not a class.', 27 | $class, 28 | ), 29 | context: $context, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableTransformerRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Rekalogika\Mapper\TransformerRegistry\SearchResult; 18 | use Symfony\Component\PropertyInfo\Type; 19 | 20 | interface WarmableTransformerRegistryInterface 21 | { 22 | /** 23 | * @param array $sourceTypes 24 | * @param array $targetTypes 25 | */ 26 | public function warmingFindBySourceAndTargetTypes( 27 | array $sourceTypes, 28 | array $targetTypes, 29 | ): SearchResult; 30 | } 31 | -------------------------------------------------------------------------------- /src/ObjectCache/Exception/NonSimpleTypeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | final class NonSimpleTypeException extends UnexpectedValueException 22 | { 23 | public function __construct(Type $type, ?Context $context = null) 24 | { 25 | parent::__construct(\sprintf('Expected a simple type, got non-simple type "%s".', TypeUtil::getDebugType($type)), context: $context); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/DynamicPropertiesDeterminer/DynamicPropertiesDeterminer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\DynamicPropertiesDeterminer; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\DynamicPropertiesDeterminerInterface; 17 | use Rekalogika\Mapper\Util\ClassUtil; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final readonly class DynamicPropertiesDeterminer implements DynamicPropertiesDeterminerInterface 23 | { 24 | #[\Override] 25 | public function allowsDynamicProperties(string $class): bool 26 | { 27 | return ClassUtil::allowsDynamicProperties($class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CacheWarmer/WarmableObjectMapperResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\CustomMapper\Exception\ObjectMapperNotFoundException; 17 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 18 | 19 | interface WarmableObjectMapperResolverInterface 20 | { 21 | /** 22 | * @param class-string $sourceClass 23 | * @param class-string $targetClass 24 | * @throws ObjectMapperNotFoundException 25 | */ 26 | public function warmingGetObjectMapper( 27 | string $sourceClass, 28 | string $targetClass, 29 | ): ServiceMethodSpecification; 30 | } 31 | -------------------------------------------------------------------------------- /src/MainTransformer/MainTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | interface MainTransformerInterface 20 | { 21 | /** 22 | * @param ?Type $sourceType If null, the source type will be guessed 23 | * @param array $targetTypes 24 | */ 25 | public function transform( 26 | mixed $source, 27 | mixed $target, 28 | ?Type $sourceType, 29 | array $targetTypes, 30 | Context $context, 31 | ?string $path = null, 32 | ): mixed; 33 | } 34 | -------------------------------------------------------------------------------- /src/CustomMapper/Exception/ObjectMapperNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class ObjectMapperNotFoundException extends UnexpectedValueException 22 | { 23 | public function __construct( 24 | string $sourceClass, 25 | string $targetClass, 26 | ) { 27 | parent::__construct(\sprintf( 28 | 'Object mapper not found for source class "%s" and target class "%s".', 29 | $sourceClass, 30 | $targetClass, 31 | )); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CustomMapper/ObjectMapperResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper; 15 | 16 | use Rekalogika\Mapper\CustomMapper\Exception\ObjectMapperNotFoundException; 17 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 18 | 19 | /** 20 | * @internal 21 | */ 22 | interface ObjectMapperResolverInterface 23 | { 24 | /** 25 | * @param class-string $sourceClass 26 | * @param class-string $targetClass 27 | * @throws ObjectMapperNotFoundException 28 | */ 29 | public function getObjectMapper( 30 | string $sourceClass, 31 | string $targetClass, 32 | ): ServiceMethodSpecification; 33 | } 34 | -------------------------------------------------------------------------------- /src/ObjectCache/Implementation/ObjectCacheFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ObjectCache\Implementation; 15 | 16 | use Rekalogika\Mapper\ObjectCache\ObjectCache; 17 | use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; 18 | use Rekalogika\Mapper\TypeResolver\TypeResolverInterface; 19 | 20 | final readonly class ObjectCacheFactory implements ObjectCacheFactoryInterface 21 | { 22 | public function __construct( 23 | private TypeResolverInterface $typeResolver, 24 | ) {} 25 | 26 | #[\Override] 27 | public function createObjectCache(): ObjectCache 28 | { 29 | return new ObjectCache($this->typeResolver); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Transformer/Exception/PropertyPathAwarePropertyInfoExtractorException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\ExceptionInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class PropertyPathAwarePropertyInfoExtractorException extends \LogicException implements ExceptionInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function __construct(string $message, string $class, string $propertyPath) 27 | { 28 | $message = \sprintf('%s, root class: "%s", property path: "%s"', $message, $class, $propertyPath); 29 | 30 | parent::__construct($message); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TransformerRegistry/TransformerRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\TransformerRegistry; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Rekalogika\Mapper\Transformer\TransformerInterface; 18 | use Symfony\Component\PropertyInfo\Type; 19 | 20 | /** 21 | * @internal 22 | */ 23 | interface TransformerRegistryInterface 24 | { 25 | public function get(string $id): TransformerInterface; 26 | 27 | /** 28 | * @param array $sourceTypes 29 | * @param array $targetTypes 30 | */ 31 | public function findBySourceAndTargetTypes( 32 | array $sourceTypes, 33 | array $targetTypes, 34 | ): SearchResult; 35 | } 36 | -------------------------------------------------------------------------------- /src/Attribute/Map.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | /** 17 | * Defines the property to be mapped from or to. 18 | */ 19 | #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] 20 | final readonly class Map 21 | { 22 | public ?string $property; 23 | 24 | /** 25 | * @param class-string|null $class 26 | */ 27 | public function __construct( 28 | null|string|false $property = null, 29 | public ?string $class = null, 30 | ) { 31 | if ($property === false) { 32 | $this->property = null; 33 | } else { 34 | $this->property = $property; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transformer/Exception/ClassNotInstantiableException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | final class ClassNotInstantiableException extends NotMappableValueException 19 | { 20 | /** 21 | * @param class-string $class 22 | */ 23 | public function __construct(string $class, ?Context $context = null) 24 | { 25 | parent::__construct( 26 | message: \sprintf('Trying to instantiate "%s", but it is not instantiable. You might solve this problem by adding an "InheritanceMap" to the class, so the mapper will know which concrete class to instantiate.', $class), 27 | context: $context, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Transformer/Exception/InvalidClassException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | final class InvalidClassException extends UnexpectedValueException 22 | { 23 | public function __construct( 24 | Type $type, 25 | Context $context, 26 | ) { 27 | parent::__construct( 28 | message: \sprintf('Trying to map to class "%s", but this is not a valid class, interface, or enum.', TypeUtil::getDebugType($type)), 29 | context: $context, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Transformer/Exception/InvalidTypeInArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\InvalidArgumentException; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | final class InvalidTypeInArgumentException extends InvalidArgumentException 22 | { 23 | public function __construct( 24 | string $printfMessage, 25 | ?Type $expectedType, 26 | ?Context $context = null, 27 | ) { 28 | parent::__construct( 29 | message: \sprintf($printfMessage, TypeUtil::getDebugType($expectedType)), 30 | context: $context, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Mapping/Implementation/MappingCacheWarmer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Mapping\Implementation; 15 | 16 | use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final readonly class MappingCacheWarmer implements CacheWarmerInterface 22 | { 23 | public function __construct( 24 | private WarmableMappingFactory $warmableMappingFactory, 25 | ) {} 26 | 27 | #[\Override] 28 | public function isOptional(): bool 29 | { 30 | return false; 31 | } 32 | 33 | #[\Override] 34 | public function warmUp(string $cacheDir, ?string $buildDir = null): array 35 | { 36 | $this->warmableMappingFactory->warmUp(); 37 | 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Transformer/TransformerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; 18 | use Symfony\Component\PropertyInfo\Type; 19 | 20 | interface TransformerInterface 21 | { 22 | public const MIXED = 'mixed'; 23 | 24 | /** 25 | * @throws RefuseToTransformException 26 | */ 27 | public function transform( 28 | mixed $source, 29 | mixed $target, 30 | ?Type $sourceType, 31 | ?Type $targetType, 32 | Context $context, 33 | ): mixed; 34 | 35 | /** 36 | * @return iterable 37 | */ 38 | public function getSupportedTransformation(): iterable; 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 25 | 26 | src 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/CacheWarmer/MappingCollection.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | final class MappingCollection 17 | { 18 | /** 19 | * @var list 20 | */ 21 | private array $classMappings = []; 22 | 23 | /** 24 | * @param class-string $source 25 | * @param class-string $target 26 | */ 27 | public function addObjectMapping(string $source, string $target): self 28 | { 29 | $this->classMappings[] = [$source, $target]; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * @return iterable 36 | */ 37 | public function getClassMappings(): iterable 38 | { 39 | yield from $this->classMappings; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Context/MapperOptions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Context; 15 | 16 | /** 17 | * @immutable 18 | */ 19 | final readonly class MapperOptions 20 | { 21 | /** 22 | * @param boolean $lazyLoading Enable lazy loading. 23 | * @param boolean $readTargetValue If disabled, values on the target side will not be read, and assumed to be null. 24 | * @param boolean $objectToObjectScalarShortCircuit Performance optimization by doing scalar to scalar transformation within `ObjectToObjectTransformer` instead of delegating to the `MainTransformer` 25 | */ 26 | public function __construct( 27 | public bool $lazyLoading = true, 28 | public bool $readTargetValue = true, 29 | public bool $objectToObjectScalarShortCircuit = true, 30 | ) {} 31 | } 32 | -------------------------------------------------------------------------------- /src/Transformer/Exception/InternalClassUnsupportedException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | final class InternalClassUnsupportedException extends NotMappableValueException 19 | { 20 | /** 21 | * @param class-string $class 22 | */ 23 | public function __construct( 24 | string $class, 25 | ?\Throwable $previous = null, 26 | ?Context $context = null, 27 | ) { 28 | parent::__construct( 29 | message: \sprintf( 30 | 'Trying to map an internal class "%s" which is not supported.', 31 | $class, 32 | ), 33 | previous: $previous, 34 | context: $context, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transformer/Exception/UnableToReadException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | final class UnableToReadException extends NotMappableValueException 19 | { 20 | public function __construct( 21 | mixed $source, 22 | string $property, 23 | ?\Throwable $previous = null, 24 | ?Context $context = null, 25 | ) { 26 | parent::__construct( 27 | message: \sprintf( 28 | 'Encountered an error when trying to read from the property "%s" from object type "%s".', 29 | $property, 30 | get_debug_type($source), 31 | ), 32 | previous: $previous, 33 | context: $context, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-present Priyadi Iman Nurcahyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /src/Transformer/Exception/UnableToWriteException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | final class UnableToWriteException extends NotMappableValueException 19 | { 20 | public function __construct( 21 | mixed $target, 22 | string $propertyName, 23 | ?\Throwable $previous = null, 24 | ?Context $context = null, 25 | ) { 26 | parent::__construct( 27 | message: \sprintf( 28 | 'Encountered an error when trying to write to the property "%s" on object type "%s".', 29 | $propertyName, 30 | get_debug_type($target), 31 | ), 32 | previous: $previous, 33 | context: $context, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Util/ServiceLocator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Util; 15 | 16 | use Psr\Container\ContainerInterface; 17 | use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; 18 | 19 | /** 20 | * Simple container for non-framework use 21 | */ 22 | final readonly class ServiceLocator implements ContainerInterface 23 | { 24 | /** 25 | * @param array $services 26 | */ 27 | public function __construct( 28 | private array $services = [], 29 | ) {} 30 | 31 | #[\Override] 32 | public function get(string $id): mixed 33 | { 34 | return $this->services[$id] ?? throw new ServiceNotFoundException($id); 35 | } 36 | 37 | #[\Override] 38 | public function has(string $id): bool 39 | { 40 | return isset($this->services[$id]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\LogicException; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final class ExtraTargetPropertyNotFoundException extends LogicException 23 | { 24 | /** 25 | * @param class-string $class 26 | */ 27 | public function __construct( 28 | string $class, 29 | string $property, 30 | Context $context, 31 | ) { 32 | $message = \sprintf( 33 | 'Mapper is called with "ExtraTargetValues", but cannot find the target property "%s" in class "%s"', 34 | $property, 35 | $class, 36 | ); 37 | 38 | parent::__construct($message, context: $context); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/DynamicPropertiesDeterminer/CachingDynamicPropertiesDeterminer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\DynamicPropertiesDeterminer; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\DynamicPropertiesDeterminerInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class CachingDynamicPropertiesDeterminer implements DynamicPropertiesDeterminerInterface 22 | { 23 | /** 24 | * @var array 25 | */ 26 | private array $cache = []; 27 | 28 | public function __construct( 29 | private readonly DynamicPropertiesDeterminerInterface $decorated, 30 | ) {} 31 | 32 | #[\Override] 33 | public function allowsDynamicProperties(string $class): bool 34 | { 35 | return $this->cache[$class] 36 | ??= $this->decorated->allowsDynamicProperties($class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CacheWarmer/MappingCache.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Symfony\Component\PropertyInfo\Type; 17 | 18 | final class MappingCache 19 | { 20 | /** 21 | * @var array 22 | */ 23 | private array $mapping = []; 24 | 25 | public function saveMapping( 26 | Type $source, 27 | Type $target, 28 | string $transformerServiceId, 29 | ): void { 30 | $hash = hash('xxh128', serialize([$source, $target, $transformerServiceId])); 31 | 32 | $this->mapping[$hash] = true; 33 | } 34 | 35 | public function containsMapping( 36 | Type $source, 37 | Type $target, 38 | string $transformerServiceId, 39 | ): bool { 40 | $hash = hash('xxh128', serialize([$source, $target, $transformerServiceId])); 41 | 42 | return isset($this->mapping[$hash]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Transformer/MainTransformerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | use Rekalogika\Mapper\Exception\LogicException; 17 | use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; 18 | 19 | trait MainTransformerAwareTrait 20 | { 21 | protected ?MainTransformerInterface $mainTransformer = null; 22 | 23 | public function withMainTransformer(MainTransformerInterface $mainTransformer): static 24 | { 25 | $clone = clone $this; 26 | $clone->mainTransformer = $mainTransformer; 27 | 28 | return $clone; 29 | } 30 | 31 | protected function getMainTransformer(): MainTransformerInterface 32 | { 33 | if ($this->mainTransformer === null) { 34 | throw new LogicException('Main transformer is not set. Call "withMainTransformer()" first.'); 35 | } 36 | 37 | return $this->mainTransformer; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Transformer/Implementation/NullToNullTransformer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Implementation; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\TransformerInterface; 18 | use Rekalogika\Mapper\Transformer\TypeMapping; 19 | use Rekalogika\Mapper\Util\TypeFactory; 20 | use Symfony\Component\PropertyInfo\Type; 21 | 22 | final readonly class NullToNullTransformer implements TransformerInterface 23 | { 24 | #[\Override] 25 | public function transform( 26 | mixed $source, 27 | mixed $target, 28 | ?Type $sourceType, 29 | ?Type $targetType, 30 | Context $context, 31 | ): mixed { 32 | return null; 33 | } 34 | 35 | #[\Override] 36 | public function getSupportedTransformation(): iterable 37 | { 38 | yield new TypeMapping(TypeFactory::null(), TypeFactory::null()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/non-debug.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 15 | use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; 16 | 17 | use function Symfony\Component\DependencyInjection\Loader\Configurator\service; 18 | 19 | return static function (ContainerConfigurator $containerConfigurator): void { 20 | $services = $containerConfigurator->services(); 21 | 22 | $services 23 | ->set('rekalogika.mapper.cache.property_info') 24 | ->parent('cache.system') 25 | ->tag('cache.pool'); 26 | 27 | $services 28 | ->set('rekalogika.mapper.property_info.cache', PropertyInfoCacheExtractor::class) 29 | ->decorate('rekalogika.mapper.property_info') 30 | ->args([ 31 | service('rekalogika.mapper.property_info.cache.inner'), 32 | service('rekalogika.mapper.cache.property_info'), 33 | ]); 34 | }; 35 | -------------------------------------------------------------------------------- /src/Context/ExtraTargetValues.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Context; 15 | 16 | /** 17 | * Adds additional values to the target object 18 | */ 19 | final readonly class ExtraTargetValues 20 | { 21 | /** 22 | * @param array> $arguments 23 | */ 24 | public function __construct( 25 | private array $arguments = [], 26 | ) {} 27 | 28 | /** 29 | * @param list $classes The class and its parent classes and interfaces. 30 | * @return array 31 | */ 32 | public function getArgumentsForClass(array $classes): array 33 | { 34 | $arguments = []; 35 | 36 | foreach ($classes as $class) { 37 | if (isset($this->arguments[$class])) { 38 | $arguments = array_merge($arguments, $this->arguments[$class]); 39 | } 40 | } 41 | 42 | return $arguments; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exception/ContextAwareExceptionTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\MainTransformer\Model\Path; 18 | 19 | trait ContextAwareExceptionTrait 20 | { 21 | public function __construct( 22 | string $message = '', 23 | int $code = 0, 24 | ?\Throwable $previous = null, 25 | protected ?Context $context = null, 26 | ) { 27 | $path = $context?->get(Path::class); 28 | 29 | $path = (string) $path; 30 | if ($path === '') { 31 | $path = '(root)'; 32 | } 33 | 34 | $message = \sprintf('%s Mapping path: "%s".', $message, $path); 35 | 36 | if ($previous !== null) { 37 | $message = \sprintf('%s Previous message: %s.', $message, $previous->getMessage()); 38 | } 39 | 40 | parent::__construct($message, $code, $previous); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/PropertyAccessInfoExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | use Symfony\Component\PropertyInfo\PropertyReadInfo; 17 | use Symfony\Component\PropertyInfo\PropertyWriteInfo; 18 | 19 | /** 20 | * @internal 21 | */ 22 | interface PropertyAccessInfoExtractorInterface 23 | { 24 | /** 25 | * @param class-string $class 26 | */ 27 | public function getReadInfo( 28 | string $class, 29 | string $property, 30 | ): ?PropertyReadInfo; 31 | 32 | /** 33 | * @param class-string $class 34 | */ 35 | public function getWriteInfo( 36 | string $class, 37 | string $property, 38 | ): ?PropertyWriteInfo; 39 | 40 | /** 41 | * @param class-string $class 42 | */ 43 | public function getConstructorInfo( 44 | string $class, 45 | string $property, 46 | ): ?PropertyWriteInfo; 47 | } 48 | -------------------------------------------------------------------------------- /src/Transformer/EagerPropertiesResolver/Implementation/ChainEagerPropertiesResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation; 15 | 16 | use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\EagerPropertiesResolverInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final readonly class ChainEagerPropertiesResolver implements EagerPropertiesResolverInterface 22 | { 23 | /** 24 | * @param iterable $resolvers 25 | */ 26 | public function __construct(private iterable $resolvers) {} 27 | 28 | #[\Override] 29 | public function getEagerProperties(string $sourceClass): array 30 | { 31 | foreach ($this->resolvers as $resolver) { 32 | $result = $resolver->getEagerProperties($sourceClass); 33 | if (!empty($result)) { 34 | return $result; 35 | } 36 | } 37 | 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CustomMapper/ObjectMapperTableEntry.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper; 15 | 16 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final readonly class ObjectMapperTableEntry 22 | { 23 | /** 24 | * @param class-string $sourceClass 25 | * @param class-string $targetClass 26 | */ 27 | public function __construct( 28 | private string $sourceClass, 29 | private string $targetClass, 30 | private ServiceMethodSpecification $serviceMethodSpecification, 31 | ) {} 32 | 33 | public function getSourceClass(): string 34 | { 35 | return $this->sourceClass; 36 | } 37 | 38 | public function getTargetClass(): string 39 | { 40 | return $this->targetClass; 41 | } 42 | 43 | public function getServiceMethodSpecification(): ServiceMethodSpecification 44 | { 45 | return $this->serviceMethodSpecification; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Transformer/Exception/SourceClassNotInInheritanceMapException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Attribute\InheritanceMap; 17 | use Rekalogika\Mapper\Context\Context; 18 | 19 | final class SourceClassNotInInheritanceMapException extends NotMappableValueException 20 | { 21 | /** 22 | * @param class-string $sourceClass 23 | * @param class-string $targetClass 24 | */ 25 | public function __construct( 26 | string $sourceClass, 27 | string $targetClass, 28 | ?Context $context = null, 29 | ) { 30 | parent::__construct( 31 | message: \sprintf( 32 | 'Trying to map to a class with an inheritance map, but source class "%s" is not found in the "%s" attribute of the target class "%s".', 33 | $sourceClass, 34 | InheritanceMap::class, 35 | $targetClass, 36 | ), 37 | context: $context, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Transformer/Exception/NullSourceButMandatoryTargetException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\RuntimeException; 18 | use Rekalogika\Mapper\Util\TypeFactory; 19 | use Rekalogika\Mapper\Util\TypeUtil; 20 | use Symfony\Component\PropertyInfo\Type; 21 | 22 | final class NullSourceButMandatoryTargetException extends RuntimeException 23 | { 24 | public function __construct( 25 | ?Type $targetType, 26 | ?\Throwable $previous = null, 27 | ?Context $context = null, 28 | ) { 29 | parent::__construct( 30 | message: \sprintf( 31 | 'The source is null, the target is mandatory & expected to be of type "%s". But no transformer is able to handle this case.', 32 | TypeUtil::getTypeString($targetType ?? TypeFactory::mixed()), 33 | ), 34 | previous: $previous, 35 | context: $context, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Transformer/Exception/PairedPropertyNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\ExceptionInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class PairedPropertyNotFoundException extends \LogicException implements ExceptionInterface 22 | { 23 | /** 24 | * @param class-string $class 25 | */ 26 | public function __construct( 27 | string $class, 28 | string $property, 29 | string $pairedClass, 30 | string $pairedProperty, 31 | ) { 32 | $message = \sprintf( 33 | 'Trying to map class "%s" property "%s" to class "%s" property "%s" according to the "Map" attribute, but the property "%s" is not found in class "%s"', 34 | $class, 35 | $property, 36 | $pairedClass, 37 | $pairedProperty, 38 | $pairedProperty, 39 | $pairedClass, 40 | ); 41 | 42 | parent::__construct($message); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/MainTransformer/Model/DebugContext.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer\Model; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * Debug context for main transformer. Used for tracing. 21 | * 22 | * @internal 23 | * @immutable 24 | */ 25 | final readonly class DebugContext 26 | { 27 | /** 28 | * @param array $targetTypes 29 | */ 30 | public function __construct( 31 | private Type $sourceType, 32 | private array $targetTypes, 33 | private bool $sourceTypeGuessed, 34 | ) {} 35 | 36 | public function getSourceType(): Type 37 | { 38 | return $this->sourceType; 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getTargetTypes(): array 45 | { 46 | return $this->targetTypes; 47 | } 48 | 49 | public function isSourceTypeGuessed(): bool 50 | { 51 | return $this->sourceTypeGuessed; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MainTransformer/Exception/CircularReferenceException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\ExceptionInterface; 18 | use Rekalogika\Mapper\Exception\RuntimeException; 19 | use Rekalogika\Mapper\Util\TypeUtil; 20 | use Symfony\Component\PropertyInfo\Type; 21 | 22 | final class CircularReferenceException extends RuntimeException implements ExceptionInterface 23 | { 24 | public function __construct( 25 | mixed $source, 26 | Type $targetType, 27 | ?Context $context = null, 28 | ?\Throwable $previous = null, 29 | ) { 30 | parent::__construct( 31 | \sprintf( 32 | 'Circular reference detected when trying to get the object of type "%s" transformed to "%s".', 33 | get_debug_type($source), 34 | TypeUtil::getDebugType($targetType), 35 | ), 36 | context: $context, 37 | previous: $previous, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DependencyInjection/CompilerPass/RemoveOptionalDefinitionPass.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\DependencyInjection\CompilerPass; 15 | 16 | use Rekalogika\Mapper\Transformer\Implementation\SymfonyUidTransformer; 17 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\Uid\Factory\UuidFactory; 20 | 21 | /** 22 | * @internal 23 | */ 24 | final readonly class RemoveOptionalDefinitionPass implements CompilerPassInterface 25 | { 26 | #[\Override] 27 | public function process(ContainerBuilder $container): void 28 | { 29 | if (!class_exists(UuidFactory::class)) { 30 | $container->removeDefinition(SymfonyUidTransformer::class); 31 | } 32 | 33 | if (!$container->hasDefinition('doctrine')) { 34 | $container->removeDefinition('rekalogika.mapper.eager_properties_resolver.doctrine'); 35 | $container->removeDefinition('rekalogika.mapper.proxy.factory.doctrine'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Attribute/InheritanceMap.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS)] 17 | final readonly class InheritanceMap 18 | { 19 | /** 20 | * @param array $map 21 | */ 22 | public function __construct( 23 | private array $map = [], 24 | ) {} 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function getMap(): array 30 | { 31 | return $this->map; 32 | } 33 | 34 | /** 35 | * @param class-string $sourceClass 36 | * @return class-string|null 37 | */ 38 | public function getTargetClassFromSourceClass(string $sourceClass): ?string 39 | { 40 | return $this->map[$sourceClass] ?? null; 41 | } 42 | 43 | /** 44 | * @param class-string $targetClass 45 | * @return class-string|null 46 | */ 47 | public function getSourceClassFromTargetClass(string $targetClass): ?string 48 | { 49 | return array_search($targetClass, $this->map, true) ?: null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Attribute/DateTimeOptions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Attribute; 15 | 16 | #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] 17 | final readonly class DateTimeOptions 18 | { 19 | /** 20 | * @param string|null $format The string format for 21 | * DateTimeInterface::format() 22 | * @param \DateTimeZone|non-empty-string|null $timeZone If specified, the DateTime will be 23 | * converted to the specified time zone. 24 | */ 25 | public function __construct( 26 | private ?string $format = null, 27 | private null|string|\DateTimeZone $timeZone = null, 28 | ) {} 29 | 30 | public function getFormat(): ?string 31 | { 32 | return $this->format; 33 | } 34 | 35 | public function getTimeZone(): null|\DateTimeZone 36 | { 37 | if ($this->timeZone === null) { 38 | return null; 39 | } 40 | 41 | if (\is_string($this->timeZone)) { 42 | return new \DateTimeZone($this->timeZone); 43 | } 44 | 45 | return $this->timeZone; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/MainTransformer/Exception/CannotFindTransformerException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 18 | use Rekalogika\Mapper\Transformer\MixedType; 19 | use Rekalogika\Mapper\Util\TypeUtil; 20 | use Symfony\Component\PropertyInfo\Type; 21 | 22 | final class CannotFindTransformerException extends UnexpectedValueException 23 | { 24 | /** 25 | * @param array $sourceTypes 26 | * @param array $targetTypes 27 | */ 28 | public function __construct(array $sourceTypes, array $targetTypes, Context $context) 29 | { 30 | $sourceTypes = TypeUtil::getDebugType($sourceTypes); 31 | $targetTypes = TypeUtil::getDebugType($targetTypes); 32 | 33 | parent::__construct( 34 | \sprintf('Cannot find a matching transformer for mapping the source types "%s" to the target types "%s".', $sourceTypes, $targetTypes), 35 | context: $context, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Transformer/Exception/MissingMemberValueTypeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\MixedType; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | final class MissingMemberValueTypeException extends MissingMemberTypeException 22 | { 23 | public function __construct( 24 | ?Type $sourceType, 25 | Type $targetType, 26 | ?Context $context = null, 27 | ) { 28 | if (null === $sourceType) { 29 | $sourceType = MixedType::instance(); 30 | } 31 | 32 | parent::__construct( 33 | message: \sprintf('Trying to map collection type "%s" to "%s", but the target does not have the type information about the value of its child members. Usually you can fix this by adding a PHPdoc to the property containing the collection type.', TypeUtil::getDebugType($sourceType), TypeUtil::getDebugType($targetType)), 34 | context: $context, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Proxy/Exception/ProxyNotSupportedException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Exception; 15 | 16 | use Rekalogika\Mapper\Exception\RuntimeException; 17 | 18 | final class ProxyNotSupportedException extends RuntimeException 19 | { 20 | private readonly string $reason; 21 | 22 | /** 23 | * @param class-string $class 24 | */ 25 | public function __construct( 26 | string $class, 27 | ?string $reason = null, 28 | ?\Throwable $previous = null, 29 | ) { 30 | parent::__construct( 31 | \sprintf( 32 | 'Creating a proxy for class "%s" is not supported.', 33 | $class, 34 | ), 35 | previous: $previous, 36 | ); 37 | 38 | $this->reason = $reason ?? $previous?->getMessage() ?? \sprintf( 39 | 'Reason is not provided, thrown by "%s", line %d.', 40 | $this->getFile(), 41 | $this->getLine(), 42 | ); 43 | } 44 | 45 | public function getReason(): string 46 | { 47 | return $this->reason; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/config') 5 | ->in(__DIR__ . '/src') 6 | ->in(__DIR__ . '/tests/bin') 7 | ->in(__DIR__ . '/tests/config') 8 | ->in(__DIR__ . '/tests/src') 9 | ->notPath('rekalogika-mapper/generated-mappings.php'); 10 | 11 | $config = new PhpCsFixer\Config(); 12 | return $config->setRules([ 13 | '@PER-CS2.0' => true, 14 | '@PER-CS2.0:risky' => true, 15 | 'fully_qualified_strict_types' => [ 16 | 'import_symbols' => true, 17 | ], 18 | 'global_namespace_import' => [ 19 | 'import_classes' => false, 20 | 'import_constants' => false, 21 | 'import_functions' => false, 22 | ], 23 | 'no_unneeded_import_alias' => true, 24 | 'no_unused_imports' => true, 25 | 'ordered_imports' => [ 26 | 'sort_algorithm' => 'alpha', 27 | 'imports_order' => ['class', 'function', 'const'] 28 | ], 29 | 'declare_strict_types' => true, 30 | 'native_function_invocation' => ['include' => ['@compiler_optimized']], 31 | 'header_comment' => [ 32 | 'header' => << 36 | 37 | For the full copyright and license information, please view the LICENSE file 38 | that was distributed with this source code. 39 | EOF, 40 | ] 41 | ]) 42 | ->setFinder($finder) 43 | ; 44 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/PropertyMetadataFactory/Util.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMetadataFactory; 15 | 16 | use Symfony\Component\PropertyInfo\Type; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final readonly class Util 22 | { 23 | private function __construct() {} 24 | 25 | /** 26 | * @param list $types 27 | * @return 'int'|'float'|'string'|'bool'|'null'|null 28 | */ 29 | public static function determineScalarType(array $types): ?string 30 | { 31 | /** @var 'int'|'float'|'string'|'bool'|'null'|null */ 32 | $scalarType = null; 33 | 34 | if (\count($types) === 1) { 35 | $propertyType = $types[0]; 36 | $propertyBuiltInType = $propertyType->getBuiltinType(); 37 | 38 | if (\in_array( 39 | $propertyBuiltInType, 40 | ['int', 'float', 'string', 'bool', 'null'], 41 | true, 42 | )) { 43 | $scalarType = $propertyBuiltInType; 44 | } 45 | } 46 | 47 | return $scalarType; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/TargetClassResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil; 15 | 16 | /** 17 | * Resolves the target type hint to the actual class name. Especially useful 18 | * for resolving abstract classes or interfaces to concrete classes. 19 | * 20 | * @internal 21 | */ 22 | interface TargetClassResolverInterface 23 | { 24 | /** 25 | * Resolves the target type hint to the actual class name. Especially useful 26 | * for resolving abstract classes or interfaces to concrete classes. 27 | * 28 | * @param class-string $sourceClass 29 | * @param class-string $targetClass 30 | * @return class-string 31 | */ 32 | public function resolveTargetClass( 33 | string $sourceClass, 34 | string $targetClass, 35 | ): string; 36 | 37 | /** 38 | * @param class-string $sourceClass 39 | * @param class-string $targetClass 40 | * @return list 41 | */ 42 | public function getAllConcreteTargetClasses( 43 | string $sourceClass, 44 | string $targetClass, 45 | ): array; 46 | } 47 | -------------------------------------------------------------------------------- /src/Transformer/AbstractTransformerDecorator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | abstract class AbstractTransformerDecorator implements TransformerInterface 20 | { 21 | public function __construct( 22 | private readonly TransformerInterface $decorated, 23 | ) {} 24 | 25 | public function getDecorated(): TransformerInterface 26 | { 27 | return $this->decorated; 28 | } 29 | 30 | #[\Override] 31 | public function transform( 32 | mixed $source, 33 | mixed $target, 34 | ?Type $sourceType, 35 | ?Type $targetType, 36 | Context $context, 37 | ): mixed { 38 | return $this->decorated->transform( 39 | $source, 40 | $target, 41 | $sourceType, 42 | $targetType, 43 | $context, 44 | ); 45 | } 46 | 47 | #[\Override] 48 | public function getSupportedTransformation(): iterable 49 | { 50 | return $this->decorated->getSupportedTransformation(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Transformer/Exception/MissingMemberKeyTypeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\MixedType; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | final class MissingMemberKeyTypeException extends MissingMemberTypeException 22 | { 23 | public function __construct( 24 | ?Type $sourceType, 25 | Type $targetType, 26 | ?Context $context = null, 27 | ) { 28 | if (null === $sourceType) { 29 | $sourceType = MixedType::instance(); 30 | } 31 | 32 | parent::__construct( 33 | message: \sprintf('Trying to map collection type "%s" to "%s", but the source member key is not the simple array-key type, and the target does not have the type information about the key of its child members. Usually you can fix this by adding a PHPdoc to the property containing the collection type.', TypeUtil::getDebugType($sourceType), TypeUtil::getDebugType($targetType)), 34 | context: $context, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Debug/TraceableMappingFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Debug; 15 | 16 | use Rekalogika\Mapper\Mapping\Mapping; 17 | use Rekalogika\Mapper\Mapping\MappingFactoryInterface; 18 | use Symfony\Contracts\Service\ResetInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final class TraceableMappingFactory implements MappingFactoryInterface, ResetInterface 24 | { 25 | private bool $mappingCollected = false; 26 | 27 | public function __construct( 28 | private readonly MappingFactoryInterface $decorated, 29 | private readonly MapperDataCollector $dataCollector, 30 | ) {} 31 | 32 | #[\Override] 33 | public function reset(): void 34 | { 35 | $this->mappingCollected = false; 36 | } 37 | 38 | #[\Override] 39 | public function getMapping(): Mapping 40 | { 41 | if ($this->mappingCollected) { 42 | return $this->decorated->getMapping(); 43 | } 44 | 45 | $mapping = $this->decorated->getMapping(); 46 | 47 | $this->dataCollector->collectMappingTable($mapping); 48 | $this->mappingCollected = true; 49 | 50 | return $mapping; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | tests/src 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | src 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Transformer/EagerPropertiesResolver/Implementation/DoctrineEagerPropertiesResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation; 15 | 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use Doctrine\Persistence\Mapping\MappingException; 18 | use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\EagerPropertiesResolverInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class DoctrineEagerPropertiesResolver implements EagerPropertiesResolverInterface 24 | { 25 | public function __construct(private ManagerRegistry $managerRegistry) {} 26 | 27 | #[\Override] 28 | public function getEagerProperties(string $sourceClass): array 29 | { 30 | $manager = $this->managerRegistry->getManagerForClass($sourceClass); 31 | 32 | if ($manager === null) { 33 | return []; 34 | } 35 | 36 | try { 37 | $metadata = $manager->getClassMetadata($sourceClass); 38 | } catch (\ReflectionException | MappingException) { 39 | return []; 40 | } 41 | 42 | return array_values($metadata->getIdentifierFieldNames()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Proxy/Metadata/PropertyMetadata.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Metadata; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final readonly class PropertyMetadata 20 | { 21 | /** 22 | * @param class-string $class 23 | * @param class-string $scopeClass 24 | */ 25 | public function __construct( 26 | private string $class, 27 | private string $scopeClass, 28 | private string $name, 29 | private string $scopeNotation, 30 | private bool $readOnly, 31 | ) {} 32 | 33 | /** 34 | * @return class-string 35 | */ 36 | public function getClass(): string 37 | { 38 | return $this->class; 39 | } 40 | 41 | /** 42 | * @return class-string 43 | */ 44 | public function getScopeClass(): string 45 | { 46 | return $this->scopeClass; 47 | } 48 | 49 | public function getName(): string 50 | { 51 | return $this->name; 52 | } 53 | 54 | public function getScopeNotation(): string 55 | { 56 | return $this->scopeNotation; 57 | } 58 | 59 | public function isReadOnly(): bool 60 | { 61 | return $this->readOnly; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | -include .env.local 3 | 4 | export APP_ENV 5 | 6 | PHP := $(shell echo $(PHP)) 7 | COMPOSER := $(shell echo $(COMPOSER)) 8 | 9 | .PHONY: test 10 | test: dump phpstan psalm phpunit 11 | 12 | .PHONY: dump 13 | dump: 14 | $(COMPOSER) dump-autoload --optimize 15 | 16 | .PHONY: phpstan 17 | phpstan: 18 | $(PHP) vendor/bin/phpstan analyse 19 | 20 | .PHONY: phpstan-baseline 21 | phpstan-baseline: 22 | $(PHP) vendor/bin/phpstan analyse --generate-baseline 23 | 24 | .PHONY: psalm 25 | psalm: 26 | $(PHP) vendor/bin/psalm --no-cache 27 | 28 | .PHONY: psalm-baseline 29 | psalm-baseline: 30 | $(PHP) vendor/bin/psalm --no-cache --update-baseline 31 | 32 | .PHONY: clean 33 | clean: 34 | rm -rf tests/var 35 | 36 | .PHONY: warmup 37 | warmup: 38 | $(PHP) tests/bin/console cache:warmup --env=test 39 | 40 | .PHONY: phpunit 41 | phpunit: clean warmup 42 | $(eval c ?=) 43 | $(PHP) vendor/bin/phpunit $(c) 44 | 45 | .PHONY: php-cs-fixer 46 | php-cs-fixer: tools/php-cs-fixer 47 | PHP_CS_FIXER_IGNORE_ENV=1 $(PHP) $< fix --config=.php-cs-fixer.dist.php --verbose --allow-risky=yes 48 | 49 | .PHONY: tools/php-cs-fixer 50 | tools/php-cs-fixer: 51 | phive install php-cs-fixer 52 | 53 | .PHONY: rector 54 | rector: 55 | $(PHP) vendor/bin/rector process > rector.log 56 | make php-cs-fixer 57 | 58 | .PHONY: serve 59 | serve: 60 | $(PHP) tests/bin/console cache:clear 61 | $(PHP) tests/bin/console asset:install tests/public/ 62 | cd tests && sh -c "APP_ENV=test $(SYMFONY) server:start --document-root=public" 63 | -------------------------------------------------------------------------------- /src/Transformer/Model/TraversableCountableWrapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Model; 15 | 16 | /** 17 | * @template TKey 18 | * @template TValue 19 | * @implements \IteratorAggregate 20 | * @internal 21 | */ 22 | final readonly class TraversableCountableWrapper implements \IteratorAggregate, \Countable 23 | { 24 | /** 25 | * @param \Traversable $traversable 26 | */ 27 | public function __construct( 28 | private \Traversable $traversable, 29 | private \Countable|int $countable, 30 | ) {} 31 | 32 | #[\Override] 33 | public function getIterator(): \Traversable 34 | { 35 | return $this->traversable; 36 | } 37 | 38 | /** 39 | * @return int<0,max> 40 | */ 41 | #[\Override] 42 | public function count(): int 43 | { 44 | if (\is_int($this->countable)) { 45 | $result = $this->countable; 46 | } else { 47 | $result = $this->countable->count(); 48 | } 49 | 50 | if ($result < 0) { 51 | throw new \LogicException('Countable must return positive integer.'); 52 | } 53 | 54 | return $result; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/CachingProxyMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Psr\Cache\CacheItemPoolInterface; 17 | use Rekalogika\Mapper\Proxy\Metadata\ClassMetadata; 18 | use Rekalogika\Mapper\Proxy\ProxyMetadataFactoryInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class CachingProxyMetadataFactory implements ProxyMetadataFactoryInterface 24 | { 25 | public function __construct( 26 | private ProxyMetadataFactoryInterface $decorated, 27 | private CacheItemPoolInterface $cache, 28 | ) {} 29 | 30 | #[\Override] 31 | public function getMetadata(string $class): ClassMetadata 32 | { 33 | $key = hash('xxh128', $class); 34 | $cacheItem = $this->cache->getItem($key); 35 | 36 | if ($cacheItem->isHit()) { 37 | /** @psalm-suppress MixedAssignment */ 38 | $result = $cacheItem->get(); 39 | 40 | if ($result instanceof ClassMetadata) { 41 | return $result; 42 | } 43 | 44 | $this->cache->deleteItem($class); 45 | } 46 | 47 | $result = $this->decorated->getMetadata($class); 48 | 49 | $cacheItem->set($result); 50 | $this->cache->save($cacheItem); 51 | 52 | return $result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/CustomMapper/Implementation/ObjectMapperTableWarmer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper\Implementation; 15 | 16 | use Rekalogika\Mapper\CustomMapper\ObjectMapperTableFactoryInterface; 17 | use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; 18 | use Symfony\Component\HttpKernel\KernelInterface; 19 | use Symfony\Component\VarExporter\VarExporter; 20 | 21 | /** 22 | * @internal 23 | */ 24 | final readonly class ObjectMapperTableWarmer implements CacheWarmerInterface 25 | { 26 | public function __construct( 27 | private ObjectMapperTableFactoryInterface $objectMapperTableFactory, 28 | private KernelInterface $kernel, 29 | ) {} 30 | 31 | #[\Override] 32 | public function isOptional(): bool 33 | { 34 | return false; 35 | } 36 | 37 | #[\Override] 38 | public function warmUp(string $cacheDir, ?string $buildDir = null): array 39 | { 40 | $mapping = VarExporter::export($this->objectMapperTableFactory->createObjectMapperTable()); 41 | file_put_contents($this->getCacheFilePath(), 'kernel->getBuildDir() . '/rekalogika_mapper_mapper_table.php'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/AttributesExtractor/CachingAttributesExtractor.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\AttributesExtractor; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\AttributesExtractorInterface; 17 | use Rekalogika\Mapper\Transformer\MetadataUtil\Model\Attributes; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final class CachingAttributesExtractor implements AttributesExtractorInterface 23 | { 24 | /** 25 | * @var array 26 | */ 27 | private array $classAttributesCache = []; 28 | 29 | /** 30 | * @var array> 31 | */ 32 | private array $propertyAttributesCache = []; 33 | 34 | public function __construct( 35 | private readonly AttributesExtractorInterface $decorated, 36 | ) {} 37 | 38 | #[\Override] 39 | public function getClassAttributes(string $class): Attributes 40 | { 41 | return $this->classAttributesCache[$class] 42 | ??= $this->decorated->getClassAttributes($class); 43 | } 44 | 45 | #[\Override] 46 | public function getPropertyAttributes(string $class, string $property): Attributes 47 | { 48 | return $this->propertyAttributesCache[$class][$property] 49 | ??= $this->decorated->getPropertyAttributes($class, $property); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TransformerRegistry/SearchResult.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\TransformerRegistry; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * @internal 21 | * @implements \IteratorAggregate 22 | */ 23 | final readonly class SearchResult implements \IteratorAggregate, \Countable 24 | { 25 | /** 26 | * @param list $sourceTypes 27 | * @param list $targetTypes 28 | * @param array $entries 29 | */ 30 | public function __construct( 31 | private array $sourceTypes, 32 | private array $targetTypes, 33 | private array $entries, 34 | ) {} 35 | 36 | #[\Override] 37 | public function count(): int 38 | { 39 | return \count($this->entries); 40 | } 41 | 42 | #[\Override] 43 | public function getIterator(): \Traversable 44 | { 45 | yield from $this->entries; 46 | } 47 | 48 | /** 49 | * @return list 50 | */ 51 | public function getSourceTypes(): array 52 | { 53 | return $this->sourceTypes; 54 | } 55 | 56 | /** 57 | * @return list 58 | */ 59 | public function getTargetTypes(): array 60 | { 61 | return $this->targetTypes; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/DoctrineProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use Rekalogika\Mapper\Proxy\Exception\ProxyNotSupportedException; 18 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class DoctrineProxyFactory implements ProxyFactoryInterface 24 | { 25 | public function __construct( 26 | private ProxyFactoryInterface $decorated, 27 | private ManagerRegistry $managerRegistry, 28 | ) {} 29 | 30 | /** 31 | * @template T of object 32 | * @param class-string $class 33 | * @param callable(T):void $initializer 34 | * @param list $eagerProperties 35 | * @return T 36 | */ 37 | #[\Override] 38 | public function createProxy( 39 | string $class, 40 | $initializer, 41 | array $eagerProperties = [], 42 | ): object { 43 | $manager = $this->managerRegistry->getManagerForClass($class); 44 | 45 | if ($manager !== null) { 46 | throw new ProxyNotSupportedException($class, reason: 'Doctrine entities do not support proxying.'); 47 | } 48 | 49 | return $this->decorated->createProxy( 50 | $class, 51 | $initializer, 52 | $eagerProperties, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ServiceMethod/ServiceMethodSpecification.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\ServiceMethod; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final readonly class ServiceMethodSpecification 20 | { 21 | public const ARGUMENT_CONTEXT = 'context'; 22 | 23 | public const ARGUMENT_MAIN_TRANSFORMER = 'main_transformer'; 24 | 25 | public const ARGUMENT_SUB_MAPPER = 'sub_mapper'; 26 | 27 | /** 28 | * @param array $extraArguments 29 | */ 30 | public function __construct( 31 | private string $serviceId, 32 | private string $method, 33 | private bool $hasExistingTarget, 34 | private bool $ignoreUninitialized, 35 | private array $extraArguments, 36 | ) {} 37 | 38 | public function getServiceId(): string 39 | { 40 | return $this->serviceId; 41 | } 42 | 43 | public function getMethod(): string 44 | { 45 | return $this->method; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getExtraArguments(): array 52 | { 53 | return $this->extraArguments; 54 | } 55 | 56 | public function hasExistingTarget(): bool 57 | { 58 | return $this->hasExistingTarget; 59 | } 60 | 61 | public function ignoreUninitialized(): bool 62 | { 63 | return $this->ignoreUninitialized; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MainTransformer/Exception/TransformerReturnsUnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer\Exception; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Debug\TraceableTransformer; 18 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 19 | use Rekalogika\Mapper\Transformer\MixedType; 20 | use Rekalogika\Mapper\Transformer\TransformerInterface; 21 | use Rekalogika\Mapper\Util\TypeUtil; 22 | use Symfony\Component\PropertyInfo\Type; 23 | 24 | final class TransformerReturnsUnexpectedValueException extends UnexpectedValueException 25 | { 26 | public function __construct( 27 | mixed $source, 28 | Type|MixedType $targetType, 29 | mixed $target, 30 | TransformerInterface $transformer, 31 | Context $context, 32 | ) { 33 | if ($transformer instanceof TraceableTransformer) { 34 | $transformer = $transformer->getDecorated(); 35 | } 36 | 37 | $message = \sprintf( 38 | 'Trying to map source type "%s" to target type "%s", but the assigned transformer "%s" returns an unexpected type "%s".', 39 | get_debug_type($source), 40 | TypeUtil::getTypeString($targetType), 41 | $transformer::class, 42 | get_debug_type($target), 43 | ); 44 | 45 | parent::__construct($message, context: $context); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SubMapper/SubMapperInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\SubMapper; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | 18 | interface SubMapperInterface 19 | { 20 | /** 21 | * Maps a source to the specified target. 22 | * 23 | * @template T of object 24 | * @param class-string|T $target 25 | * @return ($source is null ? null : T) 26 | */ 27 | public function map( 28 | ?object $source, 29 | object|string $target, 30 | ?Context $context = null, 31 | ): ?object; 32 | 33 | /** 34 | * Maps a source to the type of the specified class & property 35 | * 36 | * @param class-string|object $containing 37 | */ 38 | public function mapForProperty( 39 | ?object $source, 40 | object|string $containing, 41 | string $property, 42 | ?Context $context = null, 43 | ): mixed; 44 | 45 | /** 46 | * Add the target to the object cache 47 | */ 48 | public function cache(mixed $target): void; 49 | 50 | /** 51 | * @template T of object 52 | * @param class-string $class 53 | * @param callable(T):void $initializer 54 | * @param list $eagerProperties 55 | * @return T 56 | */ 57 | public function createProxy( 58 | string $class, 59 | $initializer, 60 | array $eagerProperties = [], 61 | ): object; 62 | } 63 | -------------------------------------------------------------------------------- /src/Transformer/TypeMapping.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer; 15 | 16 | use Rekalogika\Mapper\Exception\InvalidArgumentException; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | final readonly class TypeMapping 20 | { 21 | public function __construct( 22 | private Type|MixedType $sourceType, 23 | private Type|MixedType $targetType, 24 | private bool $variantTargetType = false, 25 | ) { 26 | if ($variantTargetType) { 27 | if ($targetType instanceof MixedType) { 28 | throw new InvalidArgumentException( 29 | 'Variant target type cannot be MixedType', 30 | ); 31 | } 32 | 33 | if ($targetType->getBuiltinType() !== Type::BUILTIN_TYPE_OBJECT) { 34 | throw new InvalidArgumentException(\sprintf( 35 | 'Variant target type must be object, %s given', 36 | $targetType->getBuiltinType(), 37 | )); 38 | } 39 | } 40 | } 41 | 42 | public function getSourceType(): Type|MixedType 43 | { 44 | return $this->sourceType; 45 | } 46 | 47 | public function getTargetType(): Type|MixedType 48 | { 49 | return $this->targetType; 50 | } 51 | 52 | public function isVariantTargetType(): bool 53 | { 54 | return $this->variantTargetType; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Util/TypeGuesser.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Util; 15 | 16 | use Rekalogika\Mapper\Exception\InvalidArgumentException; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | final readonly class TypeGuesser 20 | { 21 | private function __construct() {} 22 | 23 | public static function guessTypeFromVariable(mixed $variable): Type 24 | { 25 | $type = get_debug_type($variable); 26 | 27 | if ($type === 'array') { 28 | return TypeFactory::array(); 29 | } elseif ($type === 'bool') { 30 | return TypeFactory::bool(); 31 | } elseif ($type === 'int') { 32 | return TypeFactory::int(); 33 | } elseif ($type === 'float') { 34 | return TypeFactory::float(); 35 | } elseif ($type === 'string') { 36 | return TypeFactory::string(); 37 | } elseif ($type === 'null') { 38 | return TypeFactory::null(); 39 | } 40 | 41 | if (class_exists($type) || interface_exists($type) || enum_exists($type)) { 42 | return TypeFactory::objectOfClass($type); 43 | } 44 | 45 | if (str_starts_with($type, 'resource')) { 46 | return TypeFactory::resource(); 47 | } 48 | 49 | throw new InvalidArgumentException(\sprintf( 50 | 'Cannot determine type of variable "%s"', 51 | get_debug_type($variable), 52 | )); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/TransformerRegistry/SearchResultEntry.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\TransformerRegistry; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final readonly class SearchResultEntry 23 | { 24 | public function __construct( 25 | private int $mappingOrder, 26 | private Type|MixedType $sourceType, 27 | private Type|MixedType $targetType, 28 | private string $transformerServiceId, 29 | private bool $variantTargetType, 30 | ) {} 31 | 32 | public function getSourceType(): Type|MixedType 33 | { 34 | return $this->sourceType; 35 | } 36 | 37 | public function getTargetType(): Type|MixedType 38 | { 39 | return $this->targetType; 40 | } 41 | 42 | public function getMappingOrder(): int 43 | { 44 | return $this->mappingOrder; 45 | } 46 | 47 | public function isVariantTargetType(): bool 48 | { 49 | if ($this->targetType instanceof MixedType) { 50 | return true; 51 | } 52 | 53 | if ($this->targetType->getBuiltinType() !== Type::BUILTIN_TYPE_OBJECT) { 54 | return false; 55 | } 56 | 57 | return $this->variantTargetType; 58 | } 59 | 60 | public function getTransformerServiceId(): string 61 | { 62 | return $this->transformerServiceId; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Transformer/Implementation/CopyTransformer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Implementation; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; 18 | use Rekalogika\Mapper\Transformer\MixedType; 19 | use Rekalogika\Mapper\Transformer\TransformerInterface; 20 | use Rekalogika\Mapper\Transformer\TypeMapping; 21 | use Rekalogika\Mapper\Util\TypeCheck; 22 | use Symfony\Component\PropertyInfo\Type; 23 | 24 | final readonly class CopyTransformer implements TransformerInterface 25 | { 26 | #[\Override] 27 | public function transform( 28 | mixed $source, 29 | mixed $target, 30 | ?Type $sourceType, 31 | ?Type $targetType, 32 | Context $context, 33 | ): mixed { 34 | if ($targetType !== null && !TypeCheck::isVariableInstanceOf($source, $targetType)) { 35 | throw new RefuseToTransformException(); 36 | } 37 | 38 | if (!\is_object($source)) { 39 | return $source; 40 | } 41 | 42 | $clonable = (new \ReflectionClass($source))->isCloneable(); 43 | 44 | if (!$clonable) { 45 | return $source; 46 | } 47 | 48 | return clone $source; 49 | } 50 | 51 | #[\Override] 52 | public function getSupportedTransformation(): iterable 53 | { 54 | yield new TypeMapping(MixedType::instance(), MixedType::instance()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/TargetClassResolver/CachingTargetClassResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolver; 15 | 16 | use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolverInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class CachingTargetClassResolver implements TargetClassResolverInterface 22 | { 23 | /** 24 | * @var array> 25 | */ 26 | private array $resolveTargetClassCache = []; 27 | 28 | /** 29 | * @var array>> 30 | */ 31 | private array $getAllConcreteTargetClassesCache = []; 32 | 33 | public function __construct( 34 | private readonly TargetClassResolverInterface $decorated, 35 | ) {} 36 | 37 | #[\Override] 38 | public function resolveTargetClass( 39 | string $sourceClass, 40 | string $targetClass, 41 | ): string { 42 | return $this->resolveTargetClassCache[$sourceClass][$targetClass] 43 | ??= $this->decorated->resolveTargetClass($sourceClass, $targetClass); 44 | } 45 | 46 | #[\Override] 47 | public function getAllConcreteTargetClasses( 48 | string $sourceClass, 49 | string $targetClass, 50 | ): array { 51 | return $this->getAllConcreteTargetClassesCache[$sourceClass][$targetClass] 52 | ??= $this->decorated->getAllConcreteTargetClasses($sourceClass, $targetClass); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/DynamicPropertiesProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\Proxy\Exception\ProxyNotSupportedException; 17 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 18 | use Rekalogika\Mapper\Proxy\ProxyMetadataFactoryInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class DynamicPropertiesProxyFactory implements ProxyFactoryInterface 24 | { 25 | public function __construct( 26 | private ProxyFactoryInterface $decorated, 27 | private ProxyMetadataFactoryInterface $proxyMetadataFactory, 28 | ) {} 29 | 30 | /** 31 | * @template T of object 32 | * @param class-string $class 33 | * @param callable(T):void $initializer 34 | * @param list $eagerProperties 35 | * @return T 36 | */ 37 | #[\Override] 38 | public function createProxy( 39 | string $class, 40 | $initializer, 41 | array $eagerProperties = [], 42 | ): object { 43 | $classMetadata = $this->proxyMetadataFactory->getMetadata($class); 44 | 45 | if ($classMetadata->allowsDynamicProperties()) { 46 | throw new ProxyNotSupportedException( 47 | class: $class, 48 | reason: 'Dynamic properties are not supported.', 49 | ); 50 | } 51 | 52 | return $this->decorated->createProxy( 53 | $class, 54 | $initializer, 55 | $eagerProperties, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/CustomMapper/Implementation/ObjectMapperTableFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper\Implementation; 15 | 16 | use Rekalogika\Mapper\CustomMapper\ObjectMapperTable; 17 | use Rekalogika\Mapper\CustomMapper\ObjectMapperTableFactoryInterface; 18 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class ObjectMapperTableFactory implements ObjectMapperTableFactoryInterface 24 | { 25 | private ObjectMapperTable $objectMapperTable; 26 | 27 | public function __construct() 28 | { 29 | $this->objectMapperTable = new ObjectMapperTable(); 30 | } 31 | 32 | #[\Override] 33 | public function createObjectMapperTable(): ObjectMapperTable 34 | { 35 | return $this->objectMapperTable; 36 | } 37 | 38 | /** 39 | * @param class-string $sourceClass 40 | * @param class-string $targetClass 41 | * @param array $extraArguments 42 | */ 43 | public function addObjectMapper( 44 | string $sourceClass, 45 | string $targetClass, 46 | string $serviceId, 47 | string $method, 48 | bool $hasExistingTarget, 49 | array $extraArguments = [], 50 | ): void { 51 | $this->objectMapperTable->addObjectMapper( 52 | sourceClass: $sourceClass, 53 | targetClass: $targetClass, 54 | serviceId: $serviceId, 55 | method: $method, 56 | hasExistingTarget: $hasExistingTarget, 57 | extraArguments: $extraArguments, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SubMapper/Implementation/SubMapperFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\SubMapper\Implementation; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; 18 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 19 | use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; 20 | use Rekalogika\Mapper\SubMapper\SubMapperInterface; 21 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 22 | use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; 23 | use Symfony\Component\PropertyInfo\Type; 24 | 25 | /** 26 | * @internal 27 | */ 28 | final readonly class SubMapperFactory implements SubMapperFactoryInterface 29 | { 30 | public function __construct( 31 | private PropertyTypeExtractorInterface $propertyTypeExtractor, 32 | private PropertyAccessorInterface $propertyAccessor, 33 | private ProxyFactoryInterface $proxyFactory, 34 | ) {} 35 | 36 | #[\Override] 37 | public function createSubMapper( 38 | MainTransformerInterface $mainTransformer, 39 | mixed $source, 40 | ?Type $targetType, 41 | Context $context, 42 | ): SubMapperInterface { 43 | $subMapper = new SubMapper( 44 | propertyTypeExtractor: $this->propertyTypeExtractor, 45 | propertyAccessor: $this->propertyAccessor, 46 | proxyFactory: $this->proxyFactory, 47 | source: $source, 48 | targetType: $targetType, 49 | context: $context, 50 | ); 51 | 52 | return $subMapper->withMainTransformer($mainTransformer); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/ProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 17 | use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final readonly class ProxyFactory implements ProxyFactoryInterface 23 | { 24 | public function __construct( 25 | private VarExporterProxyFactory $varExporterProxyFactory, 26 | private PhpProxyFactory $phpProxyFactory, 27 | ) {} 28 | 29 | /** 30 | * @template T of object 31 | * @param class-string $class 32 | * @param callable(T):void $initializer 33 | * @param list $eagerProperties 34 | * @return T 35 | */ 36 | #[\Override] 37 | public function createProxy( 38 | string $class, 39 | $initializer, 40 | array $eagerProperties = [], 41 | ): object { 42 | // to preserve old behavior 43 | $reflectionClass = new \ReflectionClass($class); 44 | 45 | if (!$reflectionClass->isInstantiable()) { 46 | throw new ClassNotInstantiableException($class); 47 | } 48 | 49 | if (PHP_VERSION_ID >= 80400) { 50 | return $this->phpProxyFactory->createProxy( 51 | $class, 52 | $initializer, 53 | $eagerProperties, 54 | ); 55 | } else { 56 | return $this->varExporterProxyFactory->createProxy( 57 | $class, 58 | $initializer, 59 | $eagerProperties, 60 | ); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '13 4 * * *' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | 17 | strategy: 18 | matrix: 19 | operating-system: [ubuntu-latest] 20 | php: [ '8.2', '8.3', '8.4' ] 21 | symfony: [ '6.4.*', '7.*' ] 22 | dep: [highest,lowest] 23 | 24 | runs-on: ${{ matrix.operating-system }} 25 | 26 | name: Symfony ${{ matrix.symfony }}, ${{ matrix.dep }} deps, PHP ${{ matrix.php }}, ${{ matrix.operating-system }} 27 | 28 | steps: 29 | - uses: actions/checkout@v5 30 | 31 | - name: Install PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | extensions: intl 36 | tools: flex 37 | 38 | - name: Validate composer.json and composer.lock 39 | run: composer validate --strict 40 | 41 | - name: Cache Composer packages 42 | id: composer-cache 43 | uses: actions/cache@v4 44 | with: 45 | path: vendor 46 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-php- 49 | 50 | - name: Install dependencies 51 | uses: ramsey/composer-install@v3 52 | with: 53 | dependency-versions: ${{ matrix.dep }} 54 | composer-options: --prefer-dist --no-progress --ignore-platform-reqs 55 | env: 56 | SYMFONY_REQUIRE: ${{ matrix.symfony }} 57 | 58 | - name: Run psalm 59 | run: vendor/bin/psalm 60 | if: matrix.dep == 'highest' && matrix.symfony == '7.*' 61 | 62 | - name: Run phpstan 63 | run: vendor/bin/phpstan analyse 64 | if: matrix.dep == 'highest' 65 | 66 | - name: Run phpunit 67 | run: | 68 | export SYMFONY_DEPRECATIONS_HELPER='max[direct]=0' 69 | vendor/bin/phpunit --testdox 70 | -------------------------------------------------------------------------------- /src/Debug/TraceableObjectToObjectMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Debug; 15 | 16 | use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface; 17 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 18 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final readonly class TraceableObjectToObjectMetadataFactory implements 24 | ObjectToObjectMetadataFactoryInterface, 25 | WarmableObjectToObjectMetadataFactoryInterface 26 | { 27 | public function __construct( 28 | private ObjectToObjectMetadataFactoryInterface $decorated, 29 | private MapperDataCollector $dataCollector, 30 | ) {} 31 | 32 | #[\Override] 33 | public function createObjectToObjectMetadata( 34 | string $sourceClass, 35 | string $targetClass, 36 | ): ObjectToObjectMetadata { 37 | $metadata = $this->decorated->createObjectToObjectMetadata($sourceClass, $targetClass); 38 | 39 | $this->dataCollector->collectObjectToObjectMetadata($metadata); 40 | 41 | return $metadata; 42 | } 43 | 44 | #[\Override] 45 | public function warmingCreateObjectToObjectMetadata( 46 | string $sourceClass, 47 | string $targetClass, 48 | ): ObjectToObjectMetadata { 49 | if ($this->decorated instanceof WarmableObjectToObjectMetadataFactoryInterface) { 50 | return $this->decorated 51 | ->warmingCreateObjectToObjectMetadata($sourceClass, $targetClass); 52 | } 53 | 54 | return $this->createObjectToObjectMetadata($sourceClass, $targetClass); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/WarmableVarExporterProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\CacheWarmer\WarmableProxyFactoryInterface; 17 | use Rekalogika\Mapper\CacheWarmer\WarmableProxyRegistryInterface; 18 | use Rekalogika\Mapper\Exception\LogicException; 19 | use Rekalogika\Mapper\Proxy\ProxyGeneratorInterface; 20 | use Rekalogika\Mapper\Proxy\ProxyNamer; 21 | use Rekalogika\Mapper\Proxy\ProxyRegistryInterface; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final readonly class WarmableVarExporterProxyFactory implements 27 | WarmableProxyFactoryInterface 28 | { 29 | public function __construct( 30 | private ProxyRegistryInterface $proxyRegistry, 31 | private ProxyGeneratorInterface $proxyGenerator, 32 | ) {} 33 | 34 | /** 35 | * @param class-string $class 36 | */ 37 | #[\Override] 38 | public function warmingCreateProxy(string $class): void 39 | { 40 | $targetProxyClass = ProxyNamer::generateProxyClassName($class); 41 | 42 | $sourceCode = $this->proxyGenerator 43 | ->generateProxyCode($class, $targetProxyClass); 44 | 45 | if (!class_exists($targetProxyClass, false)) { 46 | // @phpstan-ignore ekinoBannedCode.expression 47 | eval($sourceCode); 48 | } 49 | 50 | $proxyRegistry = $this->proxyRegistry; 51 | 52 | if (!$proxyRegistry instanceof WarmableProxyRegistryInterface) { 53 | throw new LogicException( 54 | 'The proxy registry must implement WarmingProxyRegistryInterface.', 55 | ); 56 | } 57 | 58 | $proxyRegistry->warmingRegisterProxy($targetProxyClass, $sourceCode); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Transformer/Model/SplObjectStorageWrapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Model; 15 | 16 | /** 17 | * Fixes the iterator of SplObjectStorage. 18 | * 19 | * @template TKey of object 20 | * @template TValue 21 | * @implements \IteratorAggregate 22 | * @implements \ArrayAccess 23 | * @internal 24 | */ 25 | final readonly class SplObjectStorageWrapper implements 26 | \ArrayAccess, 27 | \IteratorAggregate, 28 | \Countable 29 | { 30 | /** 31 | * @param \SplObjectStorage $wrapped 32 | */ 33 | public function __construct( 34 | private \SplObjectStorage $wrapped, 35 | ) {} 36 | 37 | #[\Override] 38 | public function offsetExists(mixed $offset): bool 39 | { 40 | return $this->wrapped->contains($offset); 41 | } 42 | 43 | #[\Override] 44 | public function offsetGet(mixed $offset): mixed 45 | { 46 | return $this->wrapped->offsetGet($offset); 47 | } 48 | 49 | #[\Override] 50 | public function offsetSet(mixed $offset, mixed $value): void 51 | { 52 | \assert($offset !== null); 53 | $this->wrapped->offsetSet($offset, $value); 54 | } 55 | 56 | #[\Override] 57 | public function offsetUnset(mixed $offset): void 58 | { 59 | $this->wrapped->offsetUnset($offset); 60 | } 61 | 62 | /** 63 | * @return \Traversable 64 | */ 65 | #[\Override] 66 | public function getIterator(): \Traversable 67 | { 68 | foreach ($this->wrapped as $key) { 69 | yield $key => $this->wrapped->offsetGet($key); 70 | } 71 | } 72 | 73 | #[\Override] 74 | public function count(): int 75 | { 76 | return $this->wrapped->count(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/PhpProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 17 | use Rekalogika\Mapper\Proxy\ProxyMetadataFactoryInterface; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final readonly class PhpProxyFactory implements ProxyFactoryInterface 23 | { 24 | public function __construct( 25 | private ProxyMetadataFactoryInterface $proxyMetadataFactory, 26 | ) {} 27 | 28 | /** 29 | * @template T of object 30 | * @param class-string $class 31 | * @param callable(T):void $initializer 32 | * @param list $eagerProperties 33 | * @return T 34 | */ 35 | #[\Override] 36 | public function createProxy( 37 | string $class, 38 | $initializer, 39 | array $eagerProperties = [], 40 | ): object { 41 | $classMetadata = $this->proxyMetadataFactory->getMetadata($class); 42 | $properties = $classMetadata->getPropertyMetadatas($eagerProperties); 43 | 44 | $reflectionClass = new \ReflectionClass($class); 45 | 46 | /** 47 | * @psalm-suppress InvalidArgument 48 | * @psalm-suppress UndefinedMethod 49 | * @var T 50 | */ 51 | $proxy = $reflectionClass->newLazyGhost($initializer); 52 | 53 | foreach ($properties as $property) { 54 | $scopeClass = $property->getScopeClass(); 55 | $name = $property->getName(); 56 | 57 | $scopeReflectionClass = new \ReflectionClass($scopeClass); 58 | 59 | /** @psalm-suppress UndefinedMethod */ 60 | $scopeReflectionClass 61 | ->getProperty($name) 62 | ->skipLazyInitialization($proxy); 63 | } 64 | 65 | return $proxy; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/RekalogikaMapperBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper; 15 | 16 | use Rekalogika\Mapper\DependencyInjection\CompilerPass\DebugPass; 17 | use Rekalogika\Mapper\DependencyInjection\CompilerPass\ObjectMapperPass; 18 | use Rekalogika\Mapper\DependencyInjection\CompilerPass\PropertyMapperPass; 19 | use Rekalogika\Mapper\DependencyInjection\CompilerPass\RemoveOptionalDefinitionPass; 20 | use Rekalogika\Mapper\Proxy\ProxyAutoloaderInterface; 21 | use Symfony\Component\DependencyInjection\ContainerBuilder; 22 | use Symfony\Component\HttpKernel\Bundle\Bundle; 23 | 24 | final class RekalogikaMapperBundle extends Bundle 25 | { 26 | #[\Override] 27 | public function getPath(): string 28 | { 29 | return \dirname(__DIR__); 30 | } 31 | 32 | #[\Override] 33 | public function build(ContainerBuilder $container): void 34 | { 35 | parent::build($container); 36 | 37 | $container->addCompilerPass(new RemoveOptionalDefinitionPass()); 38 | $container->addCompilerPass(new PropertyMapperPass()); 39 | $container->addCompilerPass(new ObjectMapperPass()); 40 | 41 | if ((bool) $container->getParameter('kernel.debug')) { 42 | $container->addCompilerPass(new DebugPass()); 43 | } 44 | } 45 | 46 | #[\Override] 47 | public function boot(): void 48 | { 49 | /** @var ProxyAutoloaderInterface */ 50 | $autoloader = $this->container?->get('rekalogika.mapper.proxy_autoloader'); 51 | 52 | $autoloader->registerAutoloader(); 53 | } 54 | 55 | #[\Override] 56 | public function shutdown(): void 57 | { 58 | /** @var ProxyAutoloaderInterface */ 59 | $autoloader = $this->container?->get('rekalogika.mapper.proxy_autoloader'); 60 | 61 | $autoloader->unregisterAutoloader(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Transformer/EagerPropertiesResolver/Implementation/HeuristicsEagerPropertiesResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation; 15 | 16 | use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\EagerPropertiesResolverInterface; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final readonly class HeuristicsEagerPropertiesResolver implements EagerPropertiesResolverInterface 22 | { 23 | /** 24 | * @var array 25 | */ 26 | private array $properties; 27 | 28 | /** 29 | * @param array|null $properties 30 | */ 31 | public function __construct(?array $properties = null) 32 | { 33 | $this->properties = $properties ?? [ 34 | 'id', 35 | 'ID', 36 | 'Id', 37 | 'uuid', 38 | 'UUID', 39 | 'Uuid', 40 | 'identifier', 41 | ]; 42 | } 43 | 44 | #[\Override] 45 | public function getEagerProperties(string $sourceClass): array 46 | { 47 | $reflectionClass = new \ReflectionClass($sourceClass); 48 | 49 | foreach ($this->properties as $property) { 50 | try { 51 | $id = $reflectionClass->getProperty($property); 52 | if ($id->isPublic()) { 53 | return [$property]; 54 | } 55 | } catch (\ReflectionException) { 56 | } 57 | 58 | try { 59 | $methodName = 'get' . ucfirst($property); 60 | $id = $reflectionClass->getMethod($methodName); 61 | if ($id->isPublic()) { 62 | return [$property]; 63 | } 64 | } catch (\ReflectionException) { 65 | } 66 | } 67 | 68 | return []; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Debug/TraceableMapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Debug; 15 | 16 | use Rekalogika\Mapper\CacheWarmer\WarmableMapperInterface; 17 | use Rekalogika\Mapper\Context\Context; 18 | use Rekalogika\Mapper\Implementation\Mapper; 19 | use Rekalogika\Mapper\IterableMapperInterface; 20 | use Rekalogika\Mapper\MapperInterface; 21 | use Symfony\Component\Stopwatch\Stopwatch; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final readonly class TraceableMapper implements MapperInterface, IterableMapperInterface, WarmableMapperInterface 27 | { 28 | public function __construct( 29 | private Mapper $decorated, 30 | private Stopwatch $stopwatch, 31 | ) {} 32 | 33 | /** 34 | * @template T of object 35 | * @param class-string|T $target 36 | * @return T 37 | */ 38 | #[\Override] 39 | public function map( 40 | object $source, 41 | object|string $target, 42 | ?Context $context = null, 43 | ): object { 44 | $this->stopwatch->start('map()', 'mapper'); 45 | 46 | $result = $this->decorated->map($source, $target, $context); 47 | 48 | $this->stopwatch->stop('map()'); 49 | 50 | return $result; 51 | } 52 | 53 | #[\Override] 54 | public function mapIterable( 55 | iterable $source, 56 | string $target, 57 | ?Context $context = null, 58 | ): iterable { 59 | $this->stopwatch->start('mapIterable()', 'mapper'); 60 | 61 | $result = $this->decorated->mapIterable($source, $target, $context); 62 | 63 | $this->stopwatch->stop('mapIterable()'); 64 | 65 | return $result; 66 | } 67 | 68 | #[\Override] 69 | public function warmingMap(string $sourceClass, string $targetClass): void 70 | { 71 | $this->decorated->warmingMap($sourceClass, $targetClass); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Transformer/ObjectToObjectMetadata/Implementation/ProxyResolvingObjectToObjectMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation; 15 | 16 | use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface; 17 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 18 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; 19 | use Rekalogika\Mapper\Util\ClassUtil; 20 | 21 | /** 22 | * @internal 23 | */ 24 | final readonly class ProxyResolvingObjectToObjectMetadataFactory implements 25 | ObjectToObjectMetadataFactoryInterface, 26 | WarmableObjectToObjectMetadataFactoryInterface 27 | { 28 | public function __construct( 29 | private ObjectToObjectMetadataFactoryInterface $decorated, 30 | ) {} 31 | 32 | #[\Override] 33 | public function createObjectToObjectMetadata( 34 | string $sourceClass, 35 | string $targetClass, 36 | ): ObjectToObjectMetadata { 37 | return $this->decorated->createObjectToObjectMetadata( 38 | ClassUtil::determineRealClassFromPossibleProxy($sourceClass), 39 | $targetClass, 40 | ); 41 | } 42 | 43 | #[\Override] 44 | public function warmingCreateObjectToObjectMetadata( 45 | string $sourceClass, 46 | string $targetClass, 47 | ): ObjectToObjectMetadata { 48 | if ($this->decorated instanceof WarmableObjectToObjectMetadataFactoryInterface) { 49 | return $this->decorated->warmingCreateObjectToObjectMetadata( 50 | ClassUtil::determineRealClassFromPossibleProxy($sourceClass), 51 | $targetClass, 52 | ); 53 | } 54 | 55 | return $this->createObjectToObjectMetadata($sourceClass, $targetClass); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Transformer/Processor/ObjectProcessor/DefaultObjectProcessorFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Processor\ObjectProcessor; 15 | 16 | use Psr\Container\ContainerInterface; 17 | use Psr\Log\LoggerInterface; 18 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 19 | use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; 20 | use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; 21 | use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 22 | use Rekalogika\Mapper\Transformer\Processor\ObjectProcessorFactoryInterface; 23 | use Rekalogika\Mapper\Transformer\Processor\ObjectProcessorInterface; 24 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 25 | 26 | /** 27 | * @internal 28 | */ 29 | final class DefaultObjectProcessorFactory implements ObjectProcessorFactoryInterface 30 | { 31 | use MainTransformerAwareTrait; 32 | 33 | public function __construct( 34 | private readonly ContainerInterface $propertyMapperLocator, 35 | private readonly SubMapperFactoryInterface $subMapperFactory, 36 | private readonly ProxyFactoryInterface $proxyFactory, 37 | private readonly PropertyAccessorInterface $propertyAccessor, 38 | private readonly LoggerInterface $logger, 39 | ) {} 40 | 41 | #[\Override] 42 | public function getObjectProcessor( 43 | ObjectToObjectMetadata $metadata, 44 | ): ObjectProcessorInterface { 45 | return new ObjectProcessor( 46 | metadata: $metadata, 47 | mainTransformer: $this->getMainTransformer(), 48 | propertyMapperLocator: $this->propertyMapperLocator, 49 | subMapperFactory: $this->subMapperFactory, 50 | proxyFactory: $this->proxyFactory, 51 | propertyAccessor: $this->propertyAccessor, 52 | logger: $this->logger, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MainTransformer/Model/Path.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\MainTransformer\Model; 15 | 16 | /** 17 | * Represents the mapping path. Used for tracing purposes. 18 | * 19 | * @immutable 20 | */ 21 | final readonly class Path implements \Stringable 22 | { 23 | /** 24 | * @param list $path 25 | */ 26 | private function __construct( 27 | private array $path = [], 28 | ) {} 29 | 30 | public static function create(): self 31 | { 32 | return new self(); 33 | } 34 | 35 | #[\Override] 36 | public function __toString(): string 37 | { 38 | $result = ''; 39 | 40 | foreach ($this->path as $path) { 41 | // if path contains '[' or ']' 42 | if (str_contains($path, '[')) { 43 | // remove [ and ] 44 | $path = str_replace(['[', ']'], '', $path); 45 | $result .= \sprintf('[%s]', $path); 46 | } elseif (str_contains($path, '(')) { 47 | // remove ( and ) 48 | $path = str_replace(['(', ')'], '', $path); 49 | $result .= \sprintf('(%s)', $path); 50 | } else { 51 | $result .= '.' . $path; 52 | } 53 | } 54 | 55 | // remove leading dot 56 | 57 | if (str_starts_with($result, '.')) { 58 | $result = substr($result, 1); 59 | } 60 | 61 | return $result; 62 | } 63 | 64 | public function append(string $index): self 65 | { 66 | $path = $this->path; 67 | $path[] = $index; 68 | 69 | return new self($path); 70 | } 71 | 72 | public function getLast(): ?string 73 | { 74 | $lastKey = array_key_last($this->path); 75 | 76 | if ($lastKey === null) { 77 | return null; 78 | } 79 | 80 | return $this->path[$lastKey] ?? null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Transformer/Implementation/PresetTransformer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Implementation; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Transformer\Context\PresetMapping; 18 | use Rekalogika\Mapper\Transformer\Exception\PresetMappingNotFound; 19 | use Rekalogika\Mapper\Transformer\Exception\RefuseToTransformException; 20 | use Rekalogika\Mapper\Transformer\TransformerInterface; 21 | use Rekalogika\Mapper\Transformer\TypeMapping; 22 | use Rekalogika\Mapper\Util\TypeCheck; 23 | use Rekalogika\Mapper\Util\TypeFactory; 24 | use Symfony\Component\PropertyInfo\Type; 25 | 26 | final readonly class PresetTransformer implements TransformerInterface 27 | { 28 | #[\Override] 29 | public function transform( 30 | mixed $source, 31 | mixed $target, 32 | ?Type $sourceType, 33 | ?Type $targetType, 34 | Context $context, 35 | ): mixed { 36 | $presetMapping = $context(PresetMapping::class); 37 | 38 | if ($presetMapping === null) { 39 | throw new RefuseToTransformException(); 40 | } 41 | 42 | if (!TypeCheck::isObject($targetType)) { 43 | throw new RefuseToTransformException(); 44 | } 45 | 46 | $class = $targetType?->getClassName(); 47 | 48 | if (!\is_string($class) || !class_exists($class)) { 49 | throw new RefuseToTransformException(); 50 | } 51 | 52 | if (!\is_object($source)) { 53 | throw new RefuseToTransformException(); 54 | } 55 | 56 | try { 57 | return $presetMapping->findResult($source, $class); 58 | } catch (PresetMappingNotFound) { 59 | throw new RefuseToTransformException(); 60 | } 61 | } 62 | 63 | #[\Override] 64 | public function getSupportedTransformation(): iterable 65 | { 66 | yield new TypeMapping(TypeFactory::object(), TypeFactory::object(), true); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Transformer/Implementation/ScalarToScalarTransformer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Implementation; 15 | 16 | use Rekalogika\Mapper\Context\Context; 17 | use Rekalogika\Mapper\Exception\InvalidArgumentException; 18 | use Rekalogika\Mapper\Transformer\TransformerInterface; 19 | use Rekalogika\Mapper\Transformer\TypeMapping; 20 | use Rekalogika\Mapper\Util\TypeFactory; 21 | use Symfony\Component\PropertyInfo\Type; 22 | 23 | final readonly class ScalarToScalarTransformer implements TransformerInterface 24 | { 25 | #[\Override] 26 | public function transform( 27 | mixed $source, 28 | mixed $target, 29 | ?Type $sourceType, 30 | ?Type $targetType, 31 | Context $context, 32 | ): mixed { 33 | if (!\is_scalar($source)) { 34 | throw new InvalidArgumentException(\sprintf('Source must be scalar, "%s" given.', get_debug_type($source)), context: $context); 35 | } 36 | 37 | $targetTypeBuiltIn = $targetType?->getBuiltinType(); 38 | return match ($targetTypeBuiltIn) { 39 | Type::BUILTIN_TYPE_INT => (int) $source, 40 | Type::BUILTIN_TYPE_FLOAT => (float) $source, 41 | Type::BUILTIN_TYPE_STRING => (string) $source, 42 | Type::BUILTIN_TYPE_BOOL => (bool) $source, 43 | default => throw new InvalidArgumentException(\sprintf('Target must be scalar, "%s" given.', get_debug_type($targetType)), context: $context), 44 | }; 45 | } 46 | 47 | #[\Override] 48 | public function getSupportedTransformation(): iterable 49 | { 50 | $types = [ 51 | TypeFactory::int(), 52 | TypeFactory::float(), 53 | TypeFactory::string(), 54 | TypeFactory::bool(), 55 | ]; 56 | 57 | foreach ($types as $type1) { 58 | foreach ($types as $type2) { 59 | yield new TypeMapping($type1, $type2); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/TypeResolver/Implementation/TypeResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\TypeResolver\Implementation; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Rekalogika\Mapper\TypeResolver\TypeResolverInterface; 18 | use Rekalogika\Mapper\Util\TypeUtil; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | /** 22 | * @internal 23 | */ 24 | final readonly class TypeResolver implements TypeResolverInterface 25 | { 26 | #[\Override] 27 | public function getTypeString(Type|MixedType $type): string 28 | { 29 | return TypeUtil::getTypeString($type); 30 | } 31 | 32 | #[\Override] 33 | public function isSimpleType(Type $type): bool 34 | { 35 | return TypeUtil::isSimpleType($type); 36 | } 37 | 38 | #[\Override] 39 | public function getSimpleTypes(array|Type|MixedType $type): array 40 | { 41 | if ($type instanceof MixedType) { 42 | return [$type]; 43 | } elseif (\is_array($type)) { 44 | $simpleTypes = []; 45 | 46 | foreach ($type as $i) { 47 | foreach ($this->getSimpleTypes($i) as $simpleType) { 48 | $simpleTypes[] = $simpleType; 49 | } 50 | } 51 | 52 | return $simpleTypes; 53 | } 54 | 55 | return TypeUtil::getSimpleTypes($type); 56 | } 57 | 58 | #[\Override] 59 | public function getAcceptedTransformerInputTypeStrings(Type|MixedType $type): array 60 | { 61 | if ($type instanceof MixedType) { 62 | return ['mixed']; 63 | } 64 | 65 | return array_merge( 66 | TypeUtil::getAllTypeStrings($type, true), 67 | TypeUtil::getAttributesTypeStrings($type), 68 | ); 69 | } 70 | 71 | 72 | #[\Override] 73 | public function getAcceptedTransformerOutputTypeStrings(Type|MixedType $type): array 74 | { 75 | return $this->getAcceptedTransformerInputTypeStrings($type); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Transformer/MetadataUtil/Model/ClassMetadata.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\MetadataUtil\Model; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final readonly class ClassMetadata 20 | { 21 | /** 22 | * @param list $eagerProperties 23 | */ 24 | public function __construct( 25 | private bool $internal, 26 | private bool $instantiable, 27 | private bool $cloneable, 28 | private bool $readonly, 29 | private bool $unalterable, 30 | private bool $readableDynamicProperties, 31 | private bool $writableDynamicProperties, 32 | private Attributes $attributes, 33 | private array $eagerProperties, 34 | private int $lastModified, 35 | ) {} 36 | 37 | public function isInternal(): bool 38 | { 39 | return $this->internal; 40 | } 41 | 42 | public function isInstantiable(): bool 43 | { 44 | return $this->instantiable; 45 | } 46 | 47 | public function isCloneable(): bool 48 | { 49 | return $this->cloneable; 50 | } 51 | 52 | public function isReadonly(): bool 53 | { 54 | return $this->readonly; 55 | } 56 | 57 | public function isUnalterable(): bool 58 | { 59 | return $this->unalterable; 60 | } 61 | 62 | public function hasReadableDynamicProperties(): bool 63 | { 64 | return $this->readableDynamicProperties; 65 | } 66 | 67 | public function hasWritableDynamicProperties(): bool 68 | { 69 | return $this->writableDynamicProperties; 70 | } 71 | 72 | public function getAttributes(): Attributes 73 | { 74 | return $this->attributes; 75 | } 76 | 77 | /** 78 | * @return list 79 | */ 80 | public function getEagerProperties(): array 81 | { 82 | return $this->eagerProperties; 83 | } 84 | 85 | public function getLastModified(): int 86 | { 87 | return $this->lastModified; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/CustomMapper/Implementation/ObjectMapperResolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CustomMapper\Implementation; 15 | 16 | use Rekalogika\Mapper\CustomMapper\Exception\ObjectMapperNotFoundException; 17 | use Rekalogika\Mapper\CustomMapper\ObjectMapperResolverInterface; 18 | use Rekalogika\Mapper\CustomMapper\ObjectMapperTable; 19 | use Rekalogika\Mapper\CustomMapper\ObjectMapperTableFactoryInterface; 20 | use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; 21 | 22 | /** 23 | * @internal 24 | */ 25 | final class ObjectMapperResolver implements ObjectMapperResolverInterface 26 | { 27 | private ?ObjectMapperTable $objectMapperTable = null; 28 | 29 | /** 30 | * @var array> 31 | */ 32 | private array $objectMapperCache = []; 33 | 34 | public function __construct( 35 | private readonly ObjectMapperTableFactoryInterface $objectMapperTableFactory, 36 | ) {} 37 | 38 | private function getObjectMapperTable(): ObjectMapperTable 39 | { 40 | if ($this->objectMapperTable !== null) { 41 | return $this->objectMapperTable; 42 | } 43 | 44 | return $this->objectMapperTable = $this->objectMapperTableFactory 45 | ->createObjectMapperTable(); 46 | } 47 | 48 | #[\Override] 49 | public function getObjectMapper( 50 | string $sourceClass, 51 | string $targetClass, 52 | ): ServiceMethodSpecification { 53 | if (isset($this->objectMapperCache[$sourceClass][$targetClass])) { 54 | return $this->objectMapperCache[$sourceClass][$targetClass]; 55 | } 56 | 57 | $objectMapper = $this->getObjectMapperTable() 58 | ->getObjectMapper($sourceClass, $targetClass); 59 | 60 | if ($objectMapper === null) { 61 | throw new ObjectMapperNotFoundException($sourceClass, $targetClass); 62 | } 63 | 64 | return $this->objectMapperCache[$sourceClass][$targetClass] = $objectMapper; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/TypeResolver/TypeResolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\TypeResolver; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * @internal 21 | */ 22 | interface TypeResolverInterface 23 | { 24 | /** 25 | * Gets the string representation of a Type. 26 | */ 27 | public function getTypeString(Type|MixedType $type): string; 28 | 29 | /** 30 | * Gets all the possible simple types from a Type 31 | * 32 | * @param array|Type|MixedType $type 33 | * @return array 34 | */ 35 | public function getSimpleTypes(array|Type|MixedType $type): array; 36 | 37 | /** 38 | * Simple Type is a type that is not nullable, and does not have more 39 | * than one key type or value type. 40 | */ 41 | public function isSimpleType(Type $type): bool; 42 | 43 | /** 44 | * Example: If the variable type is 45 | * 'IteratorAggregate>', then this method 46 | * will return ['IteratorAggregate>', 47 | * 'IteratorAggregate>', 48 | * 'Traversable>', 49 | * 'Traversable>'] 50 | * 51 | * Note: IteratorAggregate extends Traversable 52 | * 53 | * @return array 54 | */ 55 | public function getAcceptedTransformerInputTypeStrings(Type|MixedType $type): array; 56 | 57 | /** 58 | * Example: If the variable type is 59 | * 'IteratorAggregate>', then this method 60 | * will return ['IteratorAggregate>', 61 | * 'IteratorAggregate>', 62 | * 'Traversable>', 63 | * 'Traversable>'] 64 | * 65 | * Note: IteratorAggregate extends Traversable 66 | * 67 | * @return array 68 | */ 69 | public function getAcceptedTransformerOutputTypeStrings(Type|MixedType $type): array; 70 | } 71 | -------------------------------------------------------------------------------- /src/CacheWarmer/MapperCacheWarmer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\CacheWarmer; 15 | 16 | use Rekalogika\Mapper\MapperInterface; 17 | use Symfony\Component\Finder\Exception\DirectoryNotFoundException; 18 | use Symfony\Component\Finder\Finder; 19 | use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; 20 | 21 | final readonly class MapperCacheWarmer implements CacheWarmerInterface 22 | { 23 | public function __construct( 24 | private string $configDir, 25 | private MapperInterface $mapper, 26 | ) {} 27 | 28 | #[\Override] 29 | public function isOptional(): bool 30 | { 31 | return true; 32 | } 33 | 34 | /** 35 | * @return iterable 36 | */ 37 | private function getObjectMapping(): iterable 38 | { 39 | $mappingCollection = new MappingCollection(); 40 | 41 | $finder = new Finder(); 42 | 43 | try { 44 | $files = $finder->files()->in($this->configDir)->name('*.php'); 45 | } catch (DirectoryNotFoundException) { 46 | return []; 47 | } 48 | 49 | foreach ($files as $file) { 50 | $realPath = $file->getRealPath(); 51 | 52 | if (false === $realPath || !file_exists($realPath)) { 53 | throw new \RuntimeException(\sprintf('The file "%s" does not exist.', $file->getRealPath())); 54 | } 55 | 56 | /** @psalm-suppress UnresolvableInclude */ 57 | $callable = require $realPath; 58 | 59 | if (!\is_callable($callable)) { 60 | throw new \RuntimeException(\sprintf('The file "%s" must return a callable.', $file->getRealPath())); 61 | } 62 | 63 | $callable($mappingCollection); 64 | } 65 | 66 | return $mappingCollection->getClassMappings(); 67 | } 68 | 69 | #[\Override] 70 | public function warmUp(string $cacheDir, ?string $buildDir = null): array 71 | { 72 | $mapper = $this->mapper; 73 | 74 | if (!$mapper instanceof WarmableMapperInterface) { 75 | return []; 76 | } 77 | 78 | foreach ($this->getObjectMapping() as [$source, $target]) { 79 | $mapper->warmingMap($source, $target); 80 | } 81 | 82 | return []; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/ProxyGenerator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\Exception\UnexpectedValueException; 17 | use Rekalogika\Mapper\Proxy\Exception\ProxyNotSupportedException; 18 | use Rekalogika\Mapper\Proxy\ProxyGeneratorInterface; 19 | use Symfony\Component\VarExporter\Exception\LogicException; 20 | use Symfony\Component\VarExporter\ProxyHelper; 21 | 22 | /** 23 | * @internal 24 | */ 25 | final readonly class ProxyGenerator implements ProxyGeneratorInterface 26 | { 27 | #[\Override] 28 | public function generateProxyCode( 29 | string $realClass, 30 | string $proxyClass, 31 | ): string { 32 | try { 33 | $proxyCode = $this->generateProxySourceCode($realClass, $proxyClass); 34 | } catch (LogicException $e) { 35 | throw new ProxyNotSupportedException($realClass, previous: $e); 36 | } 37 | 38 | return $proxyCode; 39 | } 40 | 41 | /** 42 | * @param class-string $realClass 43 | */ 44 | private function generateProxySourceCode(string $realClass, string $proxyClass): string 45 | { 46 | $targetReflection = new \ReflectionClass($realClass); 47 | 48 | // get proxy class name & namespace 49 | $shortName = preg_replace('/.*\\\\/', '', $proxyClass); 50 | 51 | if ($shortName === null) { 52 | throw new UnexpectedValueException('Short name cannot be null'); 53 | } 54 | 55 | $namespace = preg_replace('/\\\\[^\\\\]*$/', '', $proxyClass); 56 | 57 | if ($namespace === null) { 58 | throw new UnexpectedValueException('Namespace cannot be null'); 59 | } 60 | 61 | return 62 | $this->getClassHeader() . 63 | \sprintf('namespace %s;', $namespace) . "\n\n" . 64 | \sprintf( 65 | 'final %sclass %s%s', 66 | $targetReflection->isReadOnly() ? 'readonly ' : '', 67 | $shortName, 68 | ProxyHelper::generateLazyGhost($targetReflection), 69 | ); 70 | } 71 | 72 | private function getClassHeader(): string 73 | { 74 | return <<<'PHP' 75 | /* 76 | * This is a proxy class automatically generated by the rekalogika/mapper 77 | * package. Do not edit it manually. 78 | */ 79 | 80 | 81 | PHP; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Mapping/Mapping.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Mapping; 15 | 16 | use Rekalogika\Mapper\Transformer\MixedType; 17 | use Symfony\Component\PropertyInfo\Type; 18 | 19 | /** 20 | * @internal 21 | * @implements \IteratorAggregate 22 | */ 23 | final class Mapping implements \IteratorAggregate 24 | { 25 | /** 26 | * @var array 27 | */ 28 | private array $entries = []; 29 | 30 | /** 31 | * @var array>> 32 | */ 33 | private array $mappingBySourceAndTarget = []; 34 | 35 | #[\Override] 36 | public function getIterator(): \Traversable 37 | { 38 | yield from $this->entries; 39 | } 40 | 41 | public function addEntry( 42 | string $id, 43 | string $class, 44 | Type|MixedType $sourceType, 45 | Type|MixedType $targetType, 46 | string $sourceTypeString, 47 | string $targetTypeString, 48 | bool $variantTargetType, 49 | ): void { 50 | $entry = new MappingEntry( 51 | id: $id, 52 | class: $class, 53 | sourceType: $sourceType, 54 | targetType: $targetType, 55 | sourceTypeString: $sourceTypeString, 56 | targetTypeString: $targetTypeString, 57 | variantTargetType: $variantTargetType, 58 | ); 59 | 60 | $this->entries[$entry->getOrder()] = $entry; 61 | $this->mappingBySourceAndTarget[$sourceTypeString][$targetTypeString][] = $entry; 62 | } 63 | 64 | /** 65 | * @param array $sourceTypes 66 | * @param array $targetTypes 67 | * @return array 68 | */ 69 | public function getMappingBySourceAndTarget( 70 | array $sourceTypes, 71 | array $targetTypes, 72 | ): array { 73 | $result = []; 74 | 75 | foreach ($sourceTypes as $sourceType) { 76 | foreach ($targetTypes as $targetType) { 77 | if (isset($this->mappingBySourceAndTarget[$sourceType][$targetType])) { 78 | foreach ($this->mappingBySourceAndTarget[$sourceType][$targetType] as $mapper) { 79 | $result[] = $mapper; 80 | } 81 | } 82 | } 83 | } 84 | 85 | return $result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Transformer/Trait/WarmableArrayLikeTransformerTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Transformer\Trait; 15 | 16 | use Rekalogika\Mapper\CacheWarmer\WarmableArrayLikeMetadataFactoryInterface; 17 | use Rekalogika\Mapper\CacheWarmer\WarmableMainTransformerInterface; 18 | use Rekalogika\Mapper\Context\Context; 19 | use Symfony\Component\PropertyInfo\Type; 20 | 21 | trait WarmableArrayLikeTransformerTrait 22 | { 23 | public function warmingTransform( 24 | Type $sourceType, 25 | Type $targetType, 26 | Context $context, 27 | ): void { 28 | if ( 29 | !$this->arrayLikeMetadataFactory instanceof WarmableArrayLikeMetadataFactoryInterface 30 | ) { 31 | return; 32 | } 33 | 34 | // get metadata & warm it 35 | 36 | try { 37 | $metadata = $this->arrayLikeMetadataFactory 38 | ->warmingCreateArrayLikeMetadata($sourceType, $targetType); 39 | } catch (\Throwable) { 40 | return; 41 | } 42 | 43 | // ensure main transformer is warmable 44 | 45 | $mainTransformer = $this->getMainTransformer(); 46 | 47 | if (!$mainTransformer instanceof WarmableMainTransformerInterface) { 48 | return; 49 | } 50 | 51 | // warm source key to target key mapping 52 | 53 | $sourceMemberKeyTypes = $metadata->getSourceMemberKeyTypes(); 54 | $targetMemberKeyTypes = $metadata->getTargetMemberKeyTypes(); 55 | 56 | foreach ($sourceMemberKeyTypes as $sourceMemberKeyType) { 57 | $mainTransformer->warmingTransform( 58 | [$sourceMemberKeyType], 59 | $targetMemberKeyTypes, 60 | $context, 61 | ); 62 | } 63 | 64 | // warm source value to target value mapping 65 | 66 | $sourceMemberValueTypes = $metadata->getSourceMemberValueTypes(); 67 | $targetMemberValueTypes = $metadata->getTargetMemberValueTypes(); 68 | 69 | foreach ($sourceMemberValueTypes as $sourceMemberValueType) { 70 | $mainTransformer->warmingTransform( 71 | [$sourceMemberValueType], 72 | $targetMemberValueTypes, 73 | $context, 74 | ); 75 | } 76 | } 77 | 78 | public function isWarmable(): bool 79 | { 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | - config 6 | - tests/src 7 | - tests/config 8 | excludePaths: 9 | - tests/config/rekalogika-mapper 10 | checkBenevolentUnionTypes: true 11 | checkExplicitMixedMissingReturn: true 12 | checkFunctionNameCase: true 13 | checkInternalClassCaseSensitivity: true 14 | reportMaybesInPropertyPhpDocTypes: true 15 | treatPhpDocTypesAsCertain: false 16 | ignoreErrors: 17 | - '#ContainerBuilder::registerAttributeForAutoconfiguration#' 18 | - 19 | message: '#Property .* is never assigned .+ so it can be removed from the property type.#' 20 | reportUnmatched: false 21 | - 22 | identifier: catch.neverThrown 23 | reportUnmatched: false 24 | - 25 | identifier: classConstant.internalInterface 26 | path: config/* 27 | - 28 | identifier: classConstant.internalClass 29 | path: config/* 30 | - 31 | message: '#Call to an undefined method ReflectionClass<.*>::newLazyGhost#' 32 | reportUnmatched: false 33 | - 34 | message: '#Call to an undefined method ReflectionProperty::skipLazyInitialization#' 35 | reportUnmatched: false 36 | - 37 | message: '#Call to an undefined method ReflectionClass<.*>::initializeLazyObject#' 38 | reportUnmatched: false 39 | - 40 | message: '#Call to an undefined method ReflectionClass<.*>::isUninitializedLazyObject#' 41 | reportUnmatched: false 42 | - 43 | message: '#deprecated class Symfony\\Component\\PropertyInfo\\Type#' 44 | reportUnmatched: false 45 | - 46 | message: '#of interface Symfony\\Component\\PropertyInfo\\PropertyTypeExtractorInterface:#' 47 | reportUnmatched: false 48 | - 49 | message: '#deprecated method generateLazyGhost\(\) of class Symfony\\Component\\VarExporter\\ProxyHelper:#' 50 | reportUnmatched: false 51 | 52 | banned_code: 53 | non_ignorable: false 54 | rekalogika-mapper: 55 | mapperDumpFile: tests/config/rekalogika-mapper/generated-mappings.php 56 | includes: 57 | - vendor/phpstan/phpstan-phpunit/extension.neon 58 | - vendor/phpstan/phpstan-phpunit/rules.neon 59 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 60 | - vendor/bnf/phpstan-psr-container/extension.neon 61 | - vendor/ekino/phpstan-banned-code/extension.neon 62 | - vendor/dave-liddament/phpstan-php-language-extensions/extension.neon 63 | - phar://phpstan.phar/conf/bleedingEdge.neon 64 | - phpstan-extension.neon 65 | 66 | -------------------------------------------------------------------------------- /src/Proxy/Implementation/VarExporterProxyFactory.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Proxy\Implementation; 15 | 16 | use Rekalogika\Mapper\Exception\LogicException; 17 | use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; 18 | use Rekalogika\Mapper\Proxy\ProxyGeneratorInterface; 19 | use Rekalogika\Mapper\Proxy\ProxyMetadataFactoryInterface; 20 | use Rekalogika\Mapper\Proxy\ProxyNamer; 21 | use Rekalogika\Mapper\Proxy\ProxyRegistryInterface; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final readonly class VarExporterProxyFactory implements ProxyFactoryInterface 27 | { 28 | public function __construct( 29 | private ProxyRegistryInterface $proxyRegistry, 30 | private ProxyGeneratorInterface $proxyGenerator, 31 | private ProxyMetadataFactoryInterface $proxyMetadataFactory, 32 | ) {} 33 | 34 | /** 35 | * @template T of object 36 | * @param class-string $class 37 | * @param callable(T):void $initializer 38 | * @param list $eagerProperties 39 | * @return T 40 | */ 41 | #[\Override] 42 | public function createProxy( 43 | string $class, 44 | $initializer, 45 | array $eagerProperties = [], 46 | ): object { 47 | $targetProxyClass = ProxyNamer::generateProxyClassName($class); 48 | 49 | if (!class_exists($targetProxyClass)) { 50 | $sourceCode = $this->proxyGenerator 51 | ->generateProxyCode($class, $targetProxyClass); 52 | $this->proxyRegistry->registerProxy($targetProxyClass, $sourceCode); 53 | 54 | // @phpstan-ignore ekinoBannedCode.expression 55 | eval($sourceCode); 56 | 57 | // @phpstan-ignore-next-line 58 | if (!class_exists($targetProxyClass)) { 59 | throw new LogicException( 60 | \sprintf('Unable to find target proxy class "%s".', $targetProxyClass), 61 | ); 62 | } 63 | } 64 | 65 | $skippedProperties = $this->proxyMetadataFactory 66 | ->getMetadata($class) 67 | ->getVarExporterSkippedProperties($eagerProperties); 68 | 69 | /** 70 | * @psalm-suppress UndefinedMethod 71 | * @psalm-suppress MixedReturnStatement 72 | * @psalm-suppress MixedMethodCall 73 | * @var T 74 | */ 75 | return $targetProxyClass::createLazyGhost($initializer, $skippedProperties); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Context/Context.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE file 11 | * that was distributed with this source code. 12 | */ 13 | 14 | namespace Rekalogika\Mapper\Context; 15 | 16 | /** 17 | * @implements \IteratorAggregate 18 | */ 19 | final class Context implements \IteratorAggregate 20 | { 21 | /** 22 | * @param array $context 23 | */ 24 | private function __construct( 25 | private array $context = [], 26 | ) {} 27 | 28 | #[\Override] 29 | public function getIterator(): \Traversable 30 | { 31 | foreach ($this->context as $object) { 32 | yield $object; 33 | } 34 | } 35 | 36 | public static function create(object ...$objects): self 37 | { 38 | $context = []; 39 | 40 | foreach ($objects as $object) { 41 | $class = $object::class; 42 | $context[$class] = $object; 43 | } 44 | 45 | return self::createFrom($context); 46 | } 47 | 48 | /** 49 | * @param array $context 50 | */ 51 | private static function createFrom(array $context): self 52 | { 53 | return new self($context); 54 | } 55 | 56 | public function with(object ...$value): self 57 | { 58 | $clone = clone $this; 59 | 60 | foreach ($value as $object) { 61 | $class = $object::class; 62 | $clone->context[$class] = $object; 63 | } 64 | 65 | return $clone; 66 | } 67 | 68 | public function without(object|string $value): self 69 | { 70 | $class = \is_string($value) ? $value : $value::class; 71 | 72 | $clone = clone $this; 73 | 74 | if (isset($clone->context[$class])) { 75 | unset($clone->context[$class]); 76 | } 77 | 78 | return $clone; 79 | } 80 | 81 | /** 82 | * @template T of object 83 | * @param class-string $class 84 | * @return T|null 85 | */ 86 | public function get(string $class): ?object 87 | { 88 | // @phpstan-ignore-next-line 89 | return $this->context[$class] ?? null; 90 | } 91 | 92 | /** 93 | * @template T of object 94 | * @param class-string $class 95 | * @return T|null 96 | */ 97 | public function __invoke(string $class): ?object 98 | { 99 | // @phpstan-ignore-next-line 100 | return $this->context[$class] ?? null; 101 | } 102 | } 103 | --------------------------------------------------------------------------------