diff --git a/.gitignore b/.gitignore index 90ccc9f..a8c2c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ composer.lock .hg .phpunit.result.cache .phpunit.cache +temp # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file diff --git a/src/HydratorInterface.php b/src/HydratorInterface.php new file mode 100644 index 0000000..f3fa749 --- /dev/null +++ b/src/HydratorInterface.php @@ -0,0 +1,8 @@ +hydrateObject($json, clone $obj); + private KeyMappingStrategy $keyStrategy; + + public function __construct( + ?KeyMappingStrategy $keyStrategy = null, + ) { + $this->keyStrategy = $keyStrategy ?: new KeyMappingUnderscore(); } /** * @throws JsonException * @throws ReflectionException */ - public function hydrateObject(string|array $json, object $obj): object + public function hydrate(string|array $json, object|string $objOrClass): object { - $jsonArr = is_string($json) ? JsonHandler::Decode($json) : $json; - $reflectionClass = new ReflectionClass($obj); - $data = $this->processClass($reflectionClass, $jsonArr); - if ($reflectionClass->hasMethod('hydrate')) { - $obj->hydrate($data); - } else { - foreach ($data as $key => $value) { - $obj->{$key} = $value; - } - } - return $obj; + $jsonArr = is_string($json) ? $this->decode($json) : $json; + $reflectionClass = new ReflectionClass($objOrClass); + return $this->processClass($reflectionClass, $jsonArr); } /** * @throws JsonException * @throws ReflectionException */ - private function processClass(ReflectionClass $class, array $jsonArr): array + private function processClass(ReflectionClass $class, array $jsonArr): object { + $instance = $class->newInstance(); $skipAttributeCheck = ($class->getAttributes(JsonObjectAttribute::class)[0] ?? null) !== null; - $output = []; $properties = $class->getProperties(); foreach ($properties as $property) { - $output[$property->getName()] = $this->processProperty($property, $jsonArr, $skipAttributeCheck); + $property->setValue($instance, $this->processProperty($property, $jsonArr, $skipAttributeCheck)); } - return $output; + return $instance; } /** @@ -69,7 +62,7 @@ private function processProperty(ReflectionProperty $property, array $jsonArr, b /** @var JsonItemAttribute $item */ $item = $attr?->newInstance() ?? new JsonItemAttribute(); - $key = $item->key ?? $property->getName(); + $key = $item->key ?? $this->keyStrategy->from($property->getName()); if ($item->required && !array_key_exists($key, $jsonArr)) { throw new InvalidArgumentException(sprintf('required item <%s> not found', $key)); } @@ -114,9 +107,17 @@ private function handleCustomType(mixed $value, string $type): mixed if ($typeReflection->isEnum()) { return call_user_func($type.'::tryFrom', $value); } - return $this->hydrateObject( + return $this->hydrate( $value, new ($type)(), ); } + + /** + * @throws JsonException + */ + private function decode(string $json): mixed + { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } } diff --git a/src/JsonSerializerTrait.php b/src/JsonSerializer.php similarity index 83% rename from src/JsonSerializerTrait.php rename to src/JsonSerializer.php index 0436f36..37652af 100644 --- a/src/JsonSerializerTrait.php +++ b/src/JsonSerializer.php @@ -3,12 +3,22 @@ use Andrey\JsonHandler\Attributes\JsonItemAttribute; use Andrey\JsonHandler\Attributes\JsonObjectAttribute; +use Andrey\JsonHandler\KeyMapping\KeyMappingStrategy; +use Andrey\JsonHandler\KeyMapping\KeyMappingUnderscore; use JsonException; use ReflectionClass; use ReflectionException; -trait JsonSerializerTrait +readonly class JsonSerializer implements SerializerInterface { + private KeyMappingStrategy $keyStrategy; + + public function __construct( + ?KeyMappingStrategy $keyStrategy = null, + ) { + $this->keyStrategy = $keyStrategy ?: new KeyMappingUnderscore(); + } + /** * @throws ReflectionException * @throws JsonException @@ -28,7 +38,7 @@ public function serialize(object $obj): array } /** @var JsonItemAttribute $item */ $item = $attr?->newInstance() ?? new JsonItemAttribute(); - $key = $item->key ?? $property->name; + $key = $item->key ?? $this->keyStrategy->from($property->name); if ($property->getType()?->isBuiltin()) { $output[$key] = $this->handleArray($item, $property->getValue($obj)); diff --git a/src/KeyMapping/KeyMappingStrategy.php b/src/KeyMapping/KeyMappingStrategy.php new file mode 100644 index 0000000..9791869 --- /dev/null +++ b/src/KeyMapping/KeyMappingStrategy.php @@ -0,0 +1,11 @@ + Hydrate + public function from(string $key): string; + // <- Parse + public function to(string $key): string; +} diff --git a/src/KeyMapping/KeyMappingUnderscore.php b/src/KeyMapping/KeyMappingUnderscore.php new file mode 100644 index 0000000..4f42901 --- /dev/null +++ b/src/KeyMapping/KeyMappingUnderscore.php @@ -0,0 +1,60 @@ + myKey + for ($i = 0; $i < $len; $i++) { + $c = $in[$i]; + if ($c === '_') { + $c = $in[$i+1]; + // jump to next letter (skip lowercase already dealt with) + $i++; + if ($c !== '_') { + $out .= chr((ord($c) - ord('a')) + ord('A')); + } else { + $out .= $c; + } + } else { + $out .= $c; + } + } + + return $out; + } +} diff --git a/src/SerializerInterface.php b/src/SerializerInterface.php new file mode 100644 index 0000000..d301117 --- /dev/null +++ b/src/SerializerInterface.php @@ -0,0 +1,8 @@ +from('fromMyKey'); + $this->assertEquals('from_my_key', $result); + } + + public function testFromKeyStartingWithUnderscore(): void + { + $strategy = new KeyMappingUnderscore(); + $result = $strategy->from('_fromMyKey'); + $this->assertEquals('__from_my_key', $result); + } + + /** + * For pascal case maintain behavior otherwise we cannot keep + * the equality (parser -> serializer and serializer -> parser) + * + * i.e. _from_my_key => FromMyKey but from_my_key => fromMyKey + */ + public function testFromKeyPascalCase(): void + { + $strategy = new KeyMappingUnderscore(); + $result = $strategy->from('FromMyKey'); + $this->assertEquals('_from_my_key', $result); + } + + public function testToKey(): void + { + $strategy = new KeyMappingUnderscore(); + $result = $strategy->to('from_my_key'); + $this->assertEquals('fromMyKey', $result); + } + + public function testToKeyStartingWithUnderscore(): void + { + $strategy = new KeyMappingUnderscore(); + $result = $strategy->to('__from_my_key'); + $this->assertEquals('_fromMyKey', $result); + } + + public function testToKeyPascalCase(): void + { + $strategy = new KeyMappingUnderscore(); + $result = $strategy->to('_from_my_key'); + $this->assertEquals('FromMyKey', $result); + } +}