diff --git a/README.md b/README.md index 112a78d..5c4667e 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,22 @@ class Person } ``` +### Renaming keys + +If the input keys do not match the property names, you can use the `#[SourceKey]` attribute to specify the key name: + +```php +use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey; + +class Person +{ + public function __construct( + #[SourceKey('full_name')] + public readonly string $name, + ) {} +} +``` + ### Using custom mappers To map classes with your custom mapper, you need to implement `ShipMonk\InputMapper\Runtime\Mapper` interface and register it with `MapperProvider`: diff --git a/src/Compiler/Mapper/Object/SourceKey.php b/src/Compiler/Mapper/Object/SourceKey.php new file mode 100644 index 0000000..aecc6ba --- /dev/null +++ b/src/Compiler/Mapper/Object/SourceKey.php @@ -0,0 +1,17 @@ +getParameters() as $parameter) { $name = $parameter->getName(); $type = $constructorParameterTypes[$name]; + + foreach ($parameter->getAttributes(SourceKey::class) as $attribute) { + $name = $attribute->newInstance()->key; + } + $constructorParameterMapperCompilers[$name] = $this->createParameterMapperCompiler($parameter, $type, $options); } diff --git a/tests/Compiler/Mapper/Object/Data/PersonWithRenamedSourceKeysMapper.php b/tests/Compiler/Mapper/Object/Data/PersonWithRenamedSourceKeysMapper.php new file mode 100644 index 0000000..45b793f --- /dev/null +++ b/tests/Compiler/Mapper/Object/Data/PersonWithRenamedSourceKeysMapper.php @@ -0,0 +1,101 @@ + + */ +class PersonWithRenamedSourceKeysMapper implements Mapper +{ + public function __construct(private readonly MapperProvider $provider) + { + } + + /** + * @param list $path + * @throws MappingFailedException + */ + public function map(mixed $data, array $path = []): PersonInput + { + if (!is_array($data)) { + throw MappingFailedException::incorrectType($data, $path, 'array'); + } + + if (!array_key_exists('ID', $data)) { + throw MappingFailedException::missingKey($path, 'ID'); + } + + if (!array_key_exists('NAME', $data)) { + throw MappingFailedException::missingKey($path, 'NAME'); + } + + $knownKeys = ['ID' => true, 'NAME' => true, 'AGE' => true]; + $extraKeys = array_diff_key($data, $knownKeys); + + if (count($extraKeys) > 0) { + throw MappingFailedException::extraKeys($path, array_keys($extraKeys)); + } + + return new PersonInput( + $this->mapID($data['ID'], [...$path, 'ID']), + $this->mapNAME($data['NAME'], [...$path, 'NAME']), + array_key_exists('AGE', $data) ? $this->mapAGE($data['AGE'], [...$path, 'AGE']) : Optional::none($path, 'AGE'), + ); + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapID(mixed $data, array $path = []): int + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return $data; + } + + /** + * @param list $path + * @throws MappingFailedException + */ + private function mapNAME(mixed $data, array $path = []): string + { + if (!is_string($data)) { + throw MappingFailedException::incorrectType($data, $path, 'string'); + } + + return $data; + } + + /** + * @param list $path + * @return OptionalSome + * @throws MappingFailedException + */ + private function mapAGE(mixed $data, array $path = []): OptionalSome + { + if (!is_int($data)) { + throw MappingFailedException::incorrectType($data, $path, 'int'); + } + + return Optional::of($data); + } +} diff --git a/tests/Compiler/Mapper/Object/MapObjectTest.php b/tests/Compiler/Mapper/Object/MapObjectTest.php index 779ed45..7e51926 100644 --- a/tests/Compiler/Mapper/Object/MapObjectTest.php +++ b/tests/Compiler/Mapper/Object/MapObjectTest.php @@ -140,6 +140,22 @@ className: CollectionInput::class, ); } + public function testCompileWithRenamedSourceKey(): void + { + $mapperCompiler = new MapObject(PersonInput::class, [ + 'ID' => new MapInt(), + 'NAME' => new MapString(), + 'AGE' => new MapOptional(new MapInt()), + ]); + + $mapper = $this->compileMapper('PersonWithRenamedSourceKeys', $mapperCompiler); + + self::assertEquals( + new PersonInput(1, 'John', Optional::none([], 'AGE')), + $mapper->map(['ID' => 1, 'NAME' => 'John']), + ); + } + private function createMovieInputMapperCompiler(): MapperCompiler { return new MapObject(MovieInput::class, [ diff --git a/tests/Compiler/MapperFactory/Data/InputWithRenamedSourceKey.php b/tests/Compiler/MapperFactory/Data/InputWithRenamedSourceKey.php new file mode 100644 index 0000000..64b0e5c --- /dev/null +++ b/tests/Compiler/MapperFactory/Data/InputWithRenamedSourceKey.php @@ -0,0 +1,20 @@ + [ + InputWithRenamedSourceKey::class, + [], + new MapObject( + className: InputWithRenamedSourceKey::class, + constructorArgsMapperCompilers: [ + 'old_value' => new MapInt(), + 'new_value' => new MapInt(), + ], + ), + ]; + yield 'array' => [ 'array', [],