From 088ce283b04cc0f02d604ea4b6db1945d63bceec Mon Sep 17 00:00:00 2001 From: Inhere Date: Fri, 10 Nov 2023 21:29:04 +0800 Subject: [PATCH] feat: add some new base object class, add some tests --- phpunit.xml | 25 ++- src/Arr/Traits/ArrayConvertTrait.php | 59 ++++++ src/Arr/Traits/ArrayValueGetSetTrait.php | 34 ++-- src/Obj/AbstractObj.php | 34 +--- src/Obj/BaseObj.php | 11 ++ src/Obj/BaseObject.php | 239 +++++++++++++++++++++++ src/Obj/ObjectHelper.php | 80 ++++---- src/Obj/Traits/QuickInitTrait.php | 14 ++ test/Obj/ADemoPo2.php | 92 +++++++++ test/Obj/BaseObjectTest.php | 147 ++++++++++++++ 10 files changed, 635 insertions(+), 100 deletions(-) create mode 100644 src/Obj/BaseObj.php create mode 100644 src/Obj/BaseObject.php create mode 100644 test/Obj/ADemoPo2.php create mode 100644 test/Obj/BaseObjectTest.php diff --git a/phpunit.xml b/phpunit.xml index 080b327..4a35263 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,14 @@ - - - - test - - - - - src - - + + + + test + + + + + + src + + diff --git a/src/Arr/Traits/ArrayConvertTrait.php b/src/Arr/Traits/ArrayConvertTrait.php index 994fa8f..1def103 100644 --- a/src/Arr/Traits/ArrayConvertTrait.php +++ b/src/Arr/Traits/ArrayConvertTrait.php @@ -323,4 +323,63 @@ public static function toStringV2(iterable $data): string } return '{' . implode(', ', $strings) . '}'; } + + /** + * simple format array data to string. + * + * @param array $data + * @param int $depth + * + * @return string + */ + public static function toStringV3(array $data, int $depth = 3): string + { + return self::doFormat($data, $depth); + } + + private static function doFormat(array $data, int $depth = 3, int $innerDepth = 1): string + { + if (!$data) { + return '{}'; + } + + if ($depth === 0) { + return json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $count = count($data); + $isList = isset($data[0], $data[$count - 1]); + + // number list + $isNumbers = $isList && is_int($data[0]) && is_int($data[$count - 1]); + if ($isNumbers) { + return '[' . implode(',', $data) . "]"; + } + + $strings = ['{']; + $indents = str_repeat(' ', $innerDepth * 2); + + foreach ($data as $key => $value) { + $sfx = ''; + if ($value === null) { + $str = 'null'; + } elseif (is_bool($value)) { + $str = $value ? 'true' : 'false'; + } elseif (is_scalar($value)) { + $str = (string)$value; + } elseif (is_array($value)) { + $str = self::doFormat($value, $depth - 1, $innerDepth +1); + $sfx = " #len=" . count($value); + } else { + $str = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $keyString = $isList ? '' : "$key: "; + $strings[] = sprintf('%s%s%s,%s', $indents, $keyString, $str, $sfx); + } + + $strings[] = str_repeat(' ', ($innerDepth-1) * 2) . '}'; + return implode("\n", $strings); + } + } \ No newline at end of file diff --git a/src/Arr/Traits/ArrayValueGetSetTrait.php b/src/Arr/Traits/ArrayValueGetSetTrait.php index aab62bc..c3dd9ca 100644 --- a/src/Arr/Traits/ArrayValueGetSetTrait.php +++ b/src/Arr/Traits/ArrayValueGetSetTrait.php @@ -11,7 +11,6 @@ use ArrayAccess; use Toolkit\Stdlib\Php; -use Traversable; use function array_filter; use function array_shift; use function count; @@ -32,9 +31,9 @@ trait ArrayValueGetSetTrait /** * Add an element to an array using "dot" notation if it doesn't exist. * - * @param array $array + * @param array $array * @param string $key - * @param mixed $value + * @param mixed $value * * @return array */ @@ -81,9 +80,9 @@ public static function get(ArrayAccess|array $array, string $key, mixed $default * Set an array item to a given value using "dot" notation. * If no key is given to the method, the entire array will be replaced. * - * @param array $array + * @param array $array * @param string $key - * @param mixed $value + * @param mixed $value * * @return array */ @@ -114,9 +113,8 @@ public static function set(array &$array, string $key, mixed $value): array /** * Get Multi - 获取多个, 可以设置默认值 * - * @param array $data array data - * @param array $needKeys - * $needKeys = [ + * @param array $data array data + * @param array $needKeys = [ * 'name', * 'password', * 'status' => '1' @@ -131,7 +129,7 @@ public static function gets(array &$data, array $needKeys = [], bool $unsetKey = foreach ($needKeys as $key => $default) { if (is_int($key)) { - $key = $default; + $key = $default; $default = null; } @@ -163,14 +161,14 @@ public static function gets(array &$data, array $needKeys = [], bool $unsetKey = * Get data from array or object by path. * Example: `DataCollector::getByPath($array, 'foo.bar.yoo')` equals to $array['foo']['bar']['yoo']. * - * @param Traversable|array $data An array or object to get value. - * @param string $path The key path. + * @param iterable $data An array or object to get value. + * @param string $path The key path. * @param mixed|null $default - * @param string $separator Separator of paths. + * @param string $separator Separator of paths. * * @return mixed Found value, null if not exists. */ - public static function getByPath(Traversable|array $data, string $path, mixed $default = null, string $separator = '.'): mixed + public static function getByPath(iterable $data, string $path, mixed $default = null, string $separator = '.'): mixed { if (isset($data[$path])) { return $data[$path]; @@ -224,12 +222,12 @@ public static function getValueByNodes(array $data, array $nodes, mixed $default /** * setByPath * - * @param ArrayAccess|array &$data - * @param string $path - * @param mixed $value - * @param string $separator + * @param array $data + * @param string $path + * @param mixed $value + * @param string $separator */ - public static function setByPath(ArrayAccess|array &$data, string $path, mixed $value, string $separator = '.'): void + public static function setByPath(array &$data, string $path, mixed $value, string $separator = '.'): void { if (!str_contains($path, $separator)) { $data[$path] = $value; diff --git a/src/Obj/AbstractObj.php b/src/Obj/AbstractObj.php index 558641f..7a967dc 100644 --- a/src/Obj/AbstractObj.php +++ b/src/Obj/AbstractObj.php @@ -1,41 +1,11 @@ field] + * + * @var array + */ + protected array $_aliasMap = []; + + /** + * Class constructor. + * + * @param array $data + */ + public function __construct(array $data = []) + { + if ($data) { + $this->load($data); + } + $this->init(); + } + + /** + * will call init() after constructor. + */ + protected function init(): void + { + // do something... + } + + /** + * Batch set values. + * + * - Support configuration field aliases + * - Automatically convert fields to camelCase format + * + * @param array $data + * @param array $aliasMap + */ + public function load(array $data, array $aliasMap = []): void + { + if ($data) { + Obj::init($this, $data, true, $aliasMap ?: $this->_aliasMap); + } + } + + /** + * Set property value. first, will try to use setter method. + * + * @param string $field + * @param mixed $value + * @return $this + */ + public function setValue(string $field, mixed $value): static + { + Obj::init($this, [$field => $value], true, $this->_aliasMap); + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return Obj::toArray($this, false, false); + } + + /** + * Convert to array map. + * + * @param bool $filter + * + * @return array + */ + public function toMap(bool $filter = false): array + { + return $this->convToMap($filter); + } + + /** + * Convert to array map, will filter empty value. + * + * @return array + */ + public function toCleaned(): array + { + return $this->convToMap(true); + } + + /** + * @param bool $filter + * + * @return array + */ + public function toAliased(bool $filter = false): array + { + return $this->convToMap($filter, true); + } + + /** + * @var array + */ + private array $_name2alias = []; + + /** + * @param bool $filter filter empty value + * @param bool $aliased key use alias. + * + * @return array + */ + protected function convToMap(bool $filter = false, bool $aliased = false): array + { + $data = []; + $full = get_object_vars($this); + if ($aliased && $this->_aliasMap) { + $this->_name2alias = array_flip($this->_aliasMap); + } + + // filter empty value + foreach ($full as $name => $value) { + // skip un-exported field + if ($name[0] === '_') { + continue; + } + + // use alias name. + if ($aliased && isset($this->_name2alias[$name])) { + $name = $this->_name2alias[$name]; + } + + // not filter empty value + if (!$filter) { + // value is array + if (is_array($value)) { + foreach ($value as $i => $item) { + if ($item instanceof self) { + $value[$i] = $item->convToMap(false, $aliased); + } else { + $value[$i] = $item; + } + } + $data[$name] = $value; + } elseif ($value instanceof self) { + $data[$name] = $value->convToMap(false, $aliased); + } else { + $data[$name] = $value; + } + continue; + } + + // filter empty value(0 or "" or null) + if (!$value && $value !== false) { + continue; + } + + // value is array + if (is_array($value)) { + foreach ($value as $i => $item) { + if ($item instanceof self) { + $value[$i] = $item->convToMap($filter, $aliased); + } elseif (is_array($item)) { + $value[$i] = array_filter($item); + // on key is string, filter empty value + // - not handle list array. eg: value=['a', '', 'b] + } elseif (is_string($i) && !$item && $item !== false) { + unset($value[$i]); + } + } + + if ($value) { + $data[$name] = $value; + } + continue; + } + + // value is instanceof self + if ($value instanceof self) { + $data[$name] = $value->convToMap($filter, $aliased); + } else { + $data[$name] = $value; + } + } + + return $data; + } + + /** + * 转成 JSON 字符串 + * + * @param bool $filter filter empty value + * @param bool $aliased + * + * @return string + */ + public function toJson(bool $filter = true, bool $aliased = false): string + { + return Json::unescaped($this->convToMap($filter, $aliased)); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->toJson(); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toMap(true); + } + +} diff --git a/src/Obj/ObjectHelper.php b/src/Obj/ObjectHelper.php index 8902a96..1bfe607 100644 --- a/src/Obj/ObjectHelper.php +++ b/src/Obj/ObjectHelper.php @@ -17,7 +17,6 @@ use ReflectionMethod; use ReflectionNamedType; use RuntimeException; -use Toolkit\Stdlib\Helper\PhpHelper; use Toolkit\Stdlib\Str\StringHelper; use Traversable; use UnexpectedValueException; @@ -30,7 +29,6 @@ use function gzcompress; use function gzuncompress; use function is_array; -use function is_numeric; use function is_object; use function is_string; use function iterator_to_array; @@ -59,53 +57,65 @@ class ObjectHelper * - 会先尝试用 setter 方法设置属性 * - 再尝试直接设置属性 * - * @param object $object An object instance - * @param array $config + * @param object $obj An object instance + * @param array $data * @param bool $toCamel + * @param array $aliasMap * * @return object */ - public static function init(object $object, array $config, bool $toCamel = false): object + public static function init(object $obj, array $data, bool $toCamel = false, array $aliasMap = []): object { - foreach ($config as $property => $value) { - if (is_numeric($property)) { - continue; + foreach ($data as $field => $value) { + if ($aliasMap) { + $field = $aliasMap[$field] ?? $field; } if ($toCamel) { - $property = StringHelper::camelCase($property, false, '_'); + $field = StringHelper::toCamel($field); } - $setter = 'set' . ucfirst($property); - - // has setter - if (method_exists($object, $setter)) { - $object->$setter($value); - } elseif (property_exists($object, $property)) { - $object->$property = $value; - } + self::setValue($obj, $field, $value); } - return $object; + return $obj; } /** - * 给对象设置属性值 + * Set property value for object. first, will try to use setter method. * - * @param object $object - * @param array $options + * @param object $obj + * @param string $field + * @param mixed $value + * + * @return void */ - public static function configure(object $object, array $options): void + public static function setValue(object $obj, string $field, mixed $value): void { - foreach ($options as $property => $value) { - if (property_exists($object, $property)) { - $object->$property = $value; - } + // check has setter + $setter = 'set' . ucfirst($field); + if (method_exists($obj, $setter)) { + $obj->$setter($value); + } elseif (property_exists($obj, $field)) { + $obj->$field = $value; } } /** - * 给对象设置属性值 + * Set property values for object. first, will try to use setter method. + * + * @param object $obj + * @param array $data + */ + public static function configure(object $obj, array $data): void + { + foreach ($data as $field => $value) { + self::setValue($obj, $field, $value); + } + } + + /** + * Set property values for object * * @param object $object * @param array $options @@ -116,18 +126,14 @@ public static function setAttrs(object $object, array $options): void } /** - * @param object $object - * @param array $data + * Copy src object properties value to dst object * - * @throws ReflectionException + * @param object $srcObj + * @param object $dstObj */ - public static function mappingProps(object $object, array $data): void + public static function copyProps(object $srcObj, object $dstObj): void { - $rftObj = PhpHelper::reflectClass($object); - foreach ($rftObj->getProperties() as $rftProp) { - // TODO - // $typeName = $rftProp->getType() - } + self::configure($dstObj, self::toArray($srcObj)); } /** @@ -156,7 +162,7 @@ public static function decode(string $txt, bool|array $allowedClasses): mixed } /** - * PHP对象转换成为数组 + * Convert PHP object to array map, key is property name, value is property value. * * @param object $obj * @param bool $recursive diff --git a/src/Obj/Traits/QuickInitTrait.php b/src/Obj/Traits/QuickInitTrait.php index df36208..c21c712 100644 --- a/src/Obj/Traits/QuickInitTrait.php +++ b/src/Obj/Traits/QuickInitTrait.php @@ -9,6 +9,8 @@ namespace Toolkit\Stdlib\Obj\Traits; +use Toolkit\Stdlib\Json; + /** * Trait QuickInitTrait * @@ -25,4 +27,16 @@ public static function new(array $config = []): static { return new static($config); } + + /** + * from JSON string + * + * @param ?string $json + * + * @return static + */ + public static function fromJson(?string $json): static + { + return new static($json ? Json::decode($json) : []); + } } diff --git a/test/Obj/ADemoPo2.php b/test/Obj/ADemoPo2.php new file mode 100644 index 0000000..081bfab --- /dev/null +++ b/test/Obj/ADemoPo2.php @@ -0,0 +1,92 @@ + 'oneTwo2', + ]; + + /** + * @var bool + */ + public bool $bol; + + /** + * @var bool + */ + public bool $bol1; + + /** + * int + * + * @var integer + */ + public int $int; + + /** + * int2 + * + * @var integer + */ + public int $int2; + + /** + * str + * + * @var string + */ + public string $str; + + /** + * str1 + * + * @var string + */ + public string $str1; + + /** + * @var string + */ + public string $oneTwo; + + /** + * @var string + */ + public string $oneTwo2; + + /** + * arr + * + * @var array + */ + public array $arr; + + /** + * arr list + * + * @var array[] + */ + public array $arrList; + + /** + * obj + * + * @var self + */ + public ADemoPo2 $obj; + + /** + * obj list + * + * @var self[] + */ + public array $objList; + +} diff --git a/test/Obj/BaseObjectTest.php b/test/Obj/BaseObjectTest.php new file mode 100644 index 0000000..a4eea1b --- /dev/null +++ b/test/Obj/BaseObjectTest.php @@ -0,0 +1,147 @@ + 'abc']); + $this->assertEquals('abc', $po->oneTwo); + + // load real name + $po->load(['oneTwo' => 'b235', 'notExists' => '234', 'str' => '']); + $this->assertEquals('b235', $po->oneTwo); + } + + public function testArray(): void + { + // array map + $po = new ADemoPo2([ + 'arr' => [ + 'int' => 1, + 'int2' => 0, + 'str' => 'abc', + 'str1' => '', + 'arr' => [], + 'bol' => false, + ], + ]); + + $data = $po->toCleaned(); + vdump($data); + $this->assertArrayHasKey('arr', $data); + $arr = $data['arr']; + $this->assertArrayNotHasKey('int2', $arr); + $this->assertArrayNotHasKey('str1', $arr); + + $po->arr = ['a', '', 'c']; + $data = $po->toCleaned(); + vdump($data); + $this->assertArrayHasKey('arr', $data); + $arr = $data['arr']; + $this->assertCount(3, $arr); + } + + public function testSubArray(): void + { + // use snake + $po = new ADemoPo2([ + 'str' => 'abc', + 'obj' => new ADemoPo2([ + 'int' => 1, + 'int2' => 0, + 'str' => 'abc', + 'str1' => '', + 'arr' => [], + ]), + 'arrList' => [ + [ + 'sid' => 2070, + 'points' => 0, + 'give' => 0, + 'comment' => 0, + 'recharge' => 12299, + 'invitation' => 0, + 'rechargeGive' => 1, + ], + ], + ]); + + $this->assertEquals('abc', $po->str); + + vdump($data = $po->toCleaned()); + $this->assertArrayHasKey('str', $data); + + $this->assertArrayHasKey('obj', $data); + $this->assertArrayNotHasKey('int2', $data['obj']); + $this->assertArrayNotHasKey('str1', $data['obj']); + + // sub array + $this->assertArrayHasKey('arrList', $data); + $this->assertNotEmpty($subItem = $data['arrList'][0]); + $this->assertArrayHasKey('sid', $subItem); + $this->assertArrayNotHasKey('give', $subItem); + $this->assertArrayNotHasKey('comment', $subItem); + + $po->load([ + 'objList' => [ + new ADemoPo2([ + 'int' => 1, + 'int2' => 0, + 'str' => 'def', + 'str1' => '', + 'arr' => [], + ]), + ], + ]); + vdump($data = $po->toCleaned()); + + // sub object + $this->assertArrayHasKey('objList', $data); + $this->assertNotEmpty($subItem = $data['objList'][0]); + $this->assertArrayHasKey('str', $subItem); + $this->assertArrayNotHasKey('str1', $subItem); + } + + public function testUseAlias(): void + { + // use snake + $po = new ADemoPo2(['one_two' => 'abc']); + $this->assertEquals('abc', $po->oneTwo); + + // use alias name + $po->load(['ot' => 'd34'], ['ot' => 'oneTwo']); + $this->assertEquals('d34', $po->oneTwo); + + // use alias name in class _aliasMap + $po->load(['ot2' => 'a23']); + $this->assertEquals('a23', $po->oneTwo2); + + // to array map + $arr = $po->setValue('str', '')->toMap(); + vdump(get_object_vars($po), $arr); + $this->assertArrayHasKey('str', $arr); + + $arr = $po->toMap(true); + vdump($arr); + $this->assertArrayHasKey('oneTwo', $arr); + $this->assertArrayHasKey('oneTwo2', $arr); + $this->assertArrayNotHasKey('str', $arr); + + // to aliased + $arr = $po->toAliased(true); + vdump($arr); + $this->assertArrayHasKey('oneTwo', $arr); + $this->assertArrayHasKey('ot2', $arr); + $this->assertArrayNotHasKey('oneTwo2', $arr); + $this->assertArrayNotHasKey('str', $arr); + } +}