From 43a5162a15450a3afa1d671a93224a2c5d9dce54 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sun, 22 May 2022 16:53:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A7=81=EC=A0=91=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=9C=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sammo/DTO/Attr/Convert.php | 30 ++ src/sammo/DTO/Attr/JsonString.php | 11 + src/sammo/DTO/Attr/NullIsUndefined.php | 11 + src/sammo/DTO/Attr/RawName.php | 11 + src/sammo/DTO/Converter/ArrayConverter.php | 45 +++ src/sammo/DTO/Converter/Converter.php | 8 + src/sammo/DTO/Converter/DefaultConverter.php | 140 ++++++++ src/sammo/DTO/Converter/MapConverter.php | 54 +++ src/sammo/DTO/DTO.php | 106 ++++++ src/sammo/DTO/Util/Util.php | 55 ++++ tests/DTOTest.php | 329 +++++++++++++++++++ 11 files changed, 800 insertions(+) create mode 100644 src/sammo/DTO/Attr/Convert.php create mode 100644 src/sammo/DTO/Attr/JsonString.php create mode 100644 src/sammo/DTO/Attr/NullIsUndefined.php create mode 100644 src/sammo/DTO/Attr/RawName.php create mode 100644 src/sammo/DTO/Converter/ArrayConverter.php create mode 100644 src/sammo/DTO/Converter/Converter.php create mode 100644 src/sammo/DTO/Converter/DefaultConverter.php create mode 100644 src/sammo/DTO/Converter/MapConverter.php create mode 100644 src/sammo/DTO/DTO.php create mode 100644 src/sammo/DTO/Util/Util.php create mode 100644 tests/DTOTest.php diff --git a/src/sammo/DTO/Attr/Convert.php b/src/sammo/DTO/Attr/Convert.php new file mode 100644 index 00000000..ff7c211f --- /dev/null +++ b/src/sammo/DTO/Attr/Convert.php @@ -0,0 +1,30 @@ +converter = new $converterType($targetTypes, ...$args); + } + + public function convertFrom(string|array|int|float|bool|null $raw): mixed + { + return $this->converter->convertFrom($raw); + } + + public function convertTo(mixed $target): string|array|int|float|bool|null { + return $this->converter->convertTo($target); + } +} diff --git a/src/sammo/DTO/Attr/JsonString.php b/src/sammo/DTO/Attr/JsonString.php new file mode 100644 index 00000000..55c12516 --- /dev/null +++ b/src/sammo/DTO/Attr/JsonString.php @@ -0,0 +1,11 @@ +itemConverter = new $itemConverterType($itemTypes, ...$args); + } + + public function convertFrom(string|array|int|float|bool|null $raw): mixed + { + if ($raw === null && array_search('null', $this->types, true) !== false) { + return null; + } + if (!is_array($raw) || !array_is_list($raw)) { + throw new \Exception('value is not a array'); + } + return array_map(fn ($v) => $this->itemConverter->convertFrom($v), $raw); + } + + public function convertTo(mixed $data): string|array|int|float|bool|null + { + if ($data === null && array_search('null', $this->types) !== false) { + return null; + } + if (!is_array($data) || !array_is_list($data)) { + throw new \Exception('value is not a array'); + } + return array_map(fn ($v) => $this->itemConverter->convertTo($v), $data); + } +} diff --git a/src/sammo/DTO/Converter/Converter.php b/src/sammo/DTO/Converter/Converter.php new file mode 100644 index 00000000..6cd35aa0 --- /dev/null +++ b/src/sammo/DTO/Converter/Converter.php @@ -0,0 +1,8 @@ +isBacked()) { + $enum = $enumType->getMethod('tryFrom')->invoke(null, $raw); + if ($enum === null) { + return null; + } + + $success = true; + return $enum; + } + + if (!$enumType->hasCase($raw)) { + return null; + } + $success = true; + return $enumType->getCase($raw)->getValue(); + } + + if (is_subclass_of($type, DTO::class)) { + try { + $class = new \ReflectionClass($type); + $obj = $class->getMethod('fromArray')->invoke(null, $raw); + $success = true; + return $obj; + } catch (\Throwable) { + } + return null; + } + + if ($type === 'array') { + if (!is_array($raw)) { + return null; + } + if (!array_is_list($raw)) { + throw new \Exception('value is not a array'); + } + foreach ($raw as $value) { + if (is_int($value) || is_float($value) || is_string($value)) { + continue; + } + if (is_bool($value) || is_null($value)) { + continue; + } + throw new \Exception('DefaultConverter can not convert array'); + } + $success = true; + return $raw; + } + + if($type === 'int' && is_int($raw)){ + $success = true; + return $raw; + } + + if($type === 'float' && is_float($raw)){ + $success = true; + return $raw; + } + + if($type === 'string' && is_string($raw)){ + $success = true; + return $raw; + } + + if($type === 'bool' && is_bool($raw)){ + $success = true; + return $raw; + } + + return null; + } + + public function convertFrom(string|array|int|float|bool|null $raw): mixed + { + if ($raw === null && array_search('null', $this->types, true) !== false) { + return null; + } + + foreach ($this->types as $type) { + $success = false; + $value = self::convertFromItem($type, $raw, $success); + if ($success) { + return $value; + } + } + + throw new \Exception('DefaultConverter can not convert'); + } + + public function convertTo(mixed $data): string|array|int|float|bool|null + { + if ($data === null) { + return $data; + } + + if ($data instanceof \UnitEnum) { + if ($data instanceof \BackedEnum) { + return $data->value; + } + return $data->name; + } + + if ($data instanceof DTO) { + return $data->toArray(); + } + + if (is_array($data)) { + if (!array_is_list($data)) { + throw new \Exception('value is not a array'); + } + foreach ($data as $value) { + if (is_int($value) || is_float($value) || is_string($value)) { + continue; + } + if (is_bool($value) || is_null($value)) { + continue; + } + throw new \Exception('DefaultConverter can not convert array'); + } + } + + return $data; + } +} diff --git a/src/sammo/DTO/Converter/MapConverter.php b/src/sammo/DTO/Converter/MapConverter.php new file mode 100644 index 00000000..287d0665 --- /dev/null +++ b/src/sammo/DTO/Converter/MapConverter.php @@ -0,0 +1,54 @@ +itemConverter = new $itemConverterType($itemTypes, ...$args); + } + + public function convertFrom(string|array|int|float|bool|null $raw): mixed + { + if ($raw === null && array_search('null', $this->types, true) !== false) { + return null; + } + if (!is_array($raw)) { + throw new \Exception('value is not a array'); + } + + $result = []; + foreach ($raw as $key => $value) { + $result[$key] = $this->itemConverter->convertFrom($value); + } + return $result; + } + + public function convertTo(mixed $data): string|array|int|float|bool|null + { + if ($data === null && array_search('null', $this->types, true) !== false) { + return null; + } + if (!is_array($data) && !($data instanceof \Traversable)) { + throw new \Exception('value is not a array'); + } + $result = []; + foreach ($data as $key => $value) { + $result[$key] = $this->itemConverter->convertTo($value); + } + return $result; + } +} diff --git a/src/sammo/DTO/DTO.php b/src/sammo/DTO/DTO.php new file mode 100644 index 00000000..c939fd41 --- /dev/null +++ b/src/sammo/DTO/DTO.php @@ -0,0 +1,106 @@ + $array->offsetExists($key); + } else { + $keyExists = fn (string|int $key) => array_key_exists($key, $array); + } + + foreach ($reflection->getProperties( + \ReflectionProperty::IS_PUBLIC + ) as $property) { + $attrs = Util\Util::getAttrs($property); + $name = $property->getName(); + $rawName = $name; + + if (key_exists(Attr\RawName::class, $attrs)) { + $rawAttr = $attrs[Attr\RawName::class]; + $attr = new Attr\RawName(...$rawAttr->getArguments()); + $rawName = $attr->rawName; + } + + if (!$keyExists($rawName)) { + if ($property->hasDefaultValue()) { + $args[$name] = $property->getDefaultValue(); + } else if ($property->getType()->allowsNull()) { + $args[$name] = null; + } else { + throw new \Exception("Missing property: {$name}"); + } + continue; + } + + $value = $array[$rawName]; + + if (key_exists(Attr\JsonString::class, $attrs)) { + $rawAttr = $attrs[Attr\JsonString::class]; + $attr = new Attr\JsonString(...$rawAttr->getArguments()); + $value = Json::decode($value); + } + + $propTypes = Util\Util::getPropTypes($property); + if (key_exists(Attr\Convert::class, $attrs)) { + $rawAttr = $attrs[Attr\Convert::class]; + $converter = new Convert($propTypes, ...$rawAttr->getArguments()); + } else { + $converter = new Convert($propTypes, DefaultConverter::class); + } + $value = $converter->convertFrom($value); + + $args[$name] = $value; + } + + $object = $reflection->newInstanceArgs($args); + return $object; + } + + public function toArray(): array + { + $reflection = new \ReflectionClass($this::class); + $result = []; + foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $value = $property->getValue($this); + $attrs = Util\Util::getAttrs($property); + $name = $property->getName(); + + if (key_exists(Attr\RawName::class, $attrs)) { + $rawAttr = $attrs[Attr\RawName::class]; + $attr = new Attr\RawName(...$rawAttr->getArguments()); + $name = $attr->rawName; + } + + $propTypes = Util\Util::getPropTypes($property); + if (key_exists(Attr\Convert::class, $attrs)) { + $converter = new Convert($propTypes, ...$attrs[Attr\Convert::class]->getArguments()); + } else { + $converter = new Convert($propTypes, DefaultConverter::class); + } + $value = $converter->convertTo($value); + + if (key_exists(Attr\JsonString::class, $attrs)) { + $rawAttr = $attrs[Attr\JsonString::class]; + $attr = new Attr\JsonString(...$rawAttr->getArguments()); + $value = Json::encode($value, $attr->emptyItemIsArray ? JSON::EMPTY_ARRAY_IS_DICT : 0); + } + + if ($value === null && key_exists(Attr\NullIsUndefined::class, $attrs)) { + continue; + } + $result[$name] = $value; + } + return $result; + } +} diff --git a/src/sammo/DTO/Util/Util.php b/src/sammo/DTO/Util/Util.php new file mode 100644 index 00000000..5117e351 --- /dev/null +++ b/src/sammo/DTO/Util/Util.php @@ -0,0 +1,55 @@ + + */ + public static function getAttrs(\ReflectionProperty $prop): array{ + $result = []; + foreach($prop->getAttributes() as $attr){ + $result[$attr->getName()] = $attr; + } + return $result; + } + + /** + * @return array + */ + public static function getPropTypes(\ReflectionProperty $prop): array{ + $result = []; + $type = $prop->getType(); + if($type === null){ + throw new \Exception("Property {$prop->getName()} has no type"); + } + + if($type->allowsNull()){ + $result[] = 'null'; + } + + if($type instanceof \ReflectionIntersectionType){ + throw new \Exception("Intersection types are not supported"); + } + + if($type instanceof \ReflectionNamedType){ + $result[] = $type->getName(); + return $result; + } + + if($type instanceof \ReflectionUnionType){ + foreach($type->getTypes() as $type){ + $result[] = $type->getName(); + } + return $result; + } + + throw new \sammo\MustNotBeReachedException; + } +} \ No newline at end of file diff --git a/tests/DTOTest.php b/tests/DTOTest.php new file mode 100644 index 00000000..f5f90eb6 --- /dev/null +++ b/tests/DTOTest.php @@ -0,0 +1,329 @@ + '123', + 'b' => 'aa', + 'c' => false, + 'd' => null, + 'e' => 123.123, + ]; + $obj = TypeA::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testNested() + { + $rawType = [ + 'a' => '123', + 'ba' => [ + 'ba1' => 3, + 'ba2' => 4, + ] + ]; + $obj = TypeB::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testNestedJson() + { + $x = Json::encode([ + 'ba1' => 3, + 'ba2' => 4, + ]); + $rawType = [ + 'a' => '123', + 'ba' => $x + ]; + $obj = TypeC::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testEnum() + { + $rawType = [ + 'a' => '123', + 'b' => 'B', + ]; + $obj = TypeD::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testNull() + { + $rawType = [ + 'a' => null, + ]; + $obj = TypeE::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testConverter() + { + $rawType = [ + 'a' => '123', + 'b' => 123, + ]; + $obj = TypeF::fromArray($rawType); + $this->assertEquals($obj->b, 246); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testNullIsUndefined() + { + $rawType = [ + 'a' => null, + ]; + $obj = TypeE::fromArray($rawType); + $this->assertEquals($obj->a, null); + $this->assertEquals($obj->b, null); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testArr() + { + $rawType = [ + 'a' => [1, 2, 3], + 'b' => [4, 5, 6], + ]; + $obj = TypeArr::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testArrConverterErr() + { + $this->expectException(Exception::class); + $rawType = [ + 'a' => [1, 2, 3], + 'b' => [4, 5, 6], + ]; + $obj = TypeArrConverter::fromArray($rawType); + $testType = $obj->toArray(); + + + $this->assertEquals($rawType, $testType); + } + + public function testArrConverterErr2() + { + $this->expectException(Exception::class); + $rawType = [ + 'a' => [1, 2, 3], + 'b' => [4 => 6, 5 => 2, 6 => 3], + ]; + $obj = TypeArrConverter::fromArray($rawType); + $testType = $obj->toArray(); + + + $this->assertEquals($rawType, $testType); + } + + public function testArrConverter() + { + $rawType = [ + 'a' => [1, 2, 3], + 'b' => ['1', '2', '3'], + ]; + $obj = TypeArrConverter::fromArray($rawType); + $testType = $obj->toArray(); + + + $this->assertEquals($rawType, $testType); + } + + public function testMap(){ + $rawType = [ + 'a' => 1, + 'b' => [ + '1' => '1', + '2' => null, + 'ba' => '3', + ], + ]; + $obj = TypeMap::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } + + public function testNestedMap(){ + $rawType = [ + 'a' => 1, + 'b' => [ + 'aaa' => [ + 'ba1' => 1, + 'ba2' => 2, + ], + 'xed' => null, + 'ccc' => [ + 'ba1' => 3, + 'ba2' => 4, + ], + ], + ]; + $obj = TypeNestedMap::fromArray($rawType); + $testType = $obj->toArray(); + + $this->assertEquals($rawType, $testType); + } +} -- 2.54.0