diff --git a/src/Objects/Serializers/Attributes/MapInputName.php b/src/Objects/Serializers/Attributes/MapInputName.php new file mode 100644 index 0000000..7e55188 --- /dev/null +++ b/src/Objects/Serializers/Attributes/MapInputName.php @@ -0,0 +1,18 @@ +output)) { + $this->output = $input; + } + } +} \ No newline at end of file diff --git a/src/Objects/Serializers/Attributes/MapOutputName.php b/src/Objects/Serializers/Attributes/MapOutputName.php new file mode 100644 index 0000000..14bdce9 --- /dev/null +++ b/src/Objects/Serializers/Attributes/MapOutputName.php @@ -0,0 +1,18 @@ +getAttributes(MapName::class)) || + !empty($reflection->getAttributes(MapInputName::class)) || + !empty($reflection->getAttributes(MapOutputName::class))) { + return true; + } + + foreach ($reflection->getProperties() as $property) { + if (!empty($property->getAttributes(MapName::class)) || + !empty($property->getAttributes(MapInputName::class)) || + !empty($property->getAttributes(MapOutputName::class))) { + return true; + } + } + + return false; + } + + private static function serializeWithMapName(object $object): string + { + $reflection = new ReflectionClass($object); + $data = []; + + foreach ($reflection->getProperties() as $property) { + $property->setAccessible(true); + $propertyName = $property->getName(); + $value = $property->getValue($object); + + $mappedName = self::getMappedOutputName($property, $reflection) ?? $propertyName; + $data[$mappedName] = $value; + } + + $json = json_encode($data); + if ($json === false) { + throw new RuntimeException('Failed to encode JSON'); + } + + return $json; + } + + /** + * @param class-string $class + */ + private static function deserializeWithMapName(string $class, string $jsonData): mixed + { + $data = json_decode($jsonData, true); + $reflection = new ReflectionClass($class); + $constructor = $reflection->getConstructor(); + + if (!$constructor) { + return $reflection->newInstance(); + } + + $args = []; + foreach ($constructor->getParameters() as $parameter) { + $args[] = self::resolveParameterValue($parameter, $reflection, $data); + } + + return $reflection->newInstanceArgs($args); + } + + private static function resolveParameterValue(\ReflectionParameter $parameter, ReflectionClass $reflection, array $data): mixed + { + $parameterName = $parameter->getName(); + $property = $reflection->hasProperty($parameterName) ? $reflection->getProperty($parameterName) : null; + + if ($property) { + $mappedName = self::getMappedInputName($property, $reflection) ?? $parameterName; + $keyToUse = $mappedName; + } else { + $keyToUse = $parameterName; + } + + if (array_key_exists($keyToUse, $data)) { + return $data[$keyToUse]; + } + + return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + } + + private static function getMappedOutputName(ReflectionProperty $property, ReflectionClass $classReflection): ?string + { + return self::getMapperForDirection($property, $property->getName(), self::MAPPER_DIRECTION_OUTPUT) + ?? self::getMapperForDirection($classReflection, $property->getName(), self::MAPPER_DIRECTION_OUTPUT); + } + + private static function getMappedInputName(ReflectionProperty $property, ReflectionClass $classReflection): ?string + { + return self::getMapperForDirection($property, $property->getName(), self::MAPPER_DIRECTION_INPUT) + ?? self::getMapperForDirection($classReflection, $property->getName(), self::MAPPER_DIRECTION_INPUT); + } + + private static function getMapperResult(string|IPropertyMapper $mapper, string $propertyName): string + { + if (is_string($mapper)) { + if (class_exists($mapper)) { + $mapperInstance = new $mapper(); + if ($mapperInstance instanceof IPropertyMapper) { + return $mapperInstance->map($propertyName); + } + } + return $mapper; + } + + return $mapper->map($propertyName); + } + + private static function getMapperForDirection(ReflectionProperty|ReflectionClass $reflector, string $propertyName, string $direction): ?string + { + $specificAttributeClass = $direction === self::MAPPER_DIRECTION_INPUT ? MapInputName::class : MapOutputName::class; + + $specificAttributes = $reflector->getAttributes($specificAttributeClass); + if (!empty($specificAttributes)) { + return self::getMapperResult($specificAttributes[0]->newInstance()->mapper, $propertyName); + } + + $mapNameAttributes = $reflector->getAttributes(MapName::class); + if (!empty($mapNameAttributes)) { + $mapNameInstance = $mapNameAttributes[0]->newInstance(); + $mapper = $direction === self::MAPPER_DIRECTION_OUTPUT + ? ($mapNameInstance->output ?? $mapNameInstance->input) + : $mapNameInstance->input; + if ($mapper !== null) { + return self::getMapperResult($mapper, $propertyName); + } + } + + return null; + } } \ No newline at end of file diff --git a/src/Objects/Serializers/Mappers/CamelCaseMapper.php b/src/Objects/Serializers/Mappers/CamelCaseMapper.php new file mode 100644 index 0000000..01abc95 --- /dev/null +++ b/src/Objects/Serializers/Mappers/CamelCaseMapper.php @@ -0,0 +1,17 @@ +mappedName; + } +} \ No newline at end of file diff --git a/src/Objects/Serializers/Mappers/SnakeCaseMapper.php b/src/Objects/Serializers/Mappers/SnakeCaseMapper.php new file mode 100644 index 0000000..ac6b2e0 --- /dev/null +++ b/src/Objects/Serializers/Mappers/SnakeCaseMapper.php @@ -0,0 +1,17 @@ +and($object->name)->toBe('John Doe') ->and($object->age)->toBe(30); }); + + test('JsonSerializer uses MapName attributes for serialization', function () { + $object = new MapNameTestObject('John', 'Doe', 30); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'age' => 30, + ]); + }); + + test('JsonSerializer uses MapName attributes for deserialization', function () { + $json = '{"first_name":"John","last_name":"Doe","age":30}'; + $object = JsonSerializer::deserialize(MapNameTestObject::class, $json); + + expect($object)->toBeInstanceOf(MapNameTestObject::class) + ->and($object->firstName)->toBe('John') + ->and($object->lastName)->toBe('Doe') + ->and($object->age)->toBe(30); + }); + + test('JsonSerializer uses separate MapInputName and MapOutputName attributes', function () { + $object = new MapInputOutputTestObject('John Doe', 30); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'full_name' => 'John Doe', + 'age' => 30, + ]); + + $inputJson = '{"first_name":"Jane Smith","age":25}'; + $deserializedObject = JsonSerializer::deserialize(MapInputOutputTestObject::class, $inputJson); + + expect($deserializedObject)->toBeInstanceOf(MapInputOutputTestObject::class) + ->and($deserializedObject->name)->toBe('Jane Smith') + ->and($deserializedObject->age)->toBe(25); + }); + + test('JsonSerializer uses ProvidedNameMapper for custom field names', function () { + $object = new ProvidedNameTestObject('test value', 'other value'); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'custom_field_name' => 'test value', + 'otherField' => 'other value', + ]); + + $inputJson = '{"custom_field_name":"new value","otherField":"other new value"}'; + $deserializedObject = JsonSerializer::deserialize(ProvidedNameTestObject::class, $inputJson); + + expect($deserializedObject)->toBeInstanceOf(ProvidedNameTestObject::class) + ->and($deserializedObject->fieldName)->toBe('new value') + ->and($deserializedObject->otherField)->toBe('other new value'); + }); + + test('JsonSerializer uses class-level MapName attributes', function () { + $object = new ClassLevelMapNameObject('John', 'Doe', 30); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'user_age' => 30, + ]); + + $inputJson = '{"first_name":"Jane","last_name":"Smith","user_age":25}'; + $deserializedObject = JsonSerializer::deserialize(ClassLevelMapNameObject::class, $inputJson); + + expect($deserializedObject)->toBeInstanceOf(ClassLevelMapNameObject::class) + ->and($deserializedObject->firstName)->toBe('Jane') + ->and($deserializedObject->lastName)->toBe('Smith') + ->and($deserializedObject->userAge)->toBe(25); + }); + + test('JsonSerializer uses MapName with input and output parameters', function () { + $object = new MapNameInputOutputTestObject('John Doe', 'Jane', 30); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'output_name' => 'John Doe', + 'first_name' => 'Jane', + 'age' => 30, + ]); + + $inputJson = '{"input_name":"Jane Smith","first_name":"Mary","age":25}'; + $deserializedObject = JsonSerializer::deserialize(MapNameInputOutputTestObject::class, $inputJson); + + expect($deserializedObject)->toBeInstanceOf(MapNameInputOutputTestObject::class) + ->and($deserializedObject->name)->toBe('Jane Smith') + ->and($deserializedObject->firstName)->toBe('Mary') + ->and($deserializedObject->age)->toBe(25); + }); + + test('JsonSerializer falls back to Symfony serializer when no MapName attributes are present', function () { + $object = new TestObject('John Doe', 30); + $json = JsonSerializer::serialize($object); + + expect($json)->toBeJson() + ->and(json_decode($json, true))->toMatchArray([ + 'name' => 'John Doe', + 'age' => 30, + ]); + + $deserializedObject = JsonSerializer::deserialize(TestObject::class, $json); + expect($deserializedObject)->toBeInstanceOf(TestObject::class) + ->and($deserializedObject->name)->toBe('John Doe') + ->and($deserializedObject->age)->toBe(30); + }); }); \ No newline at end of file