Skip to content

Commit ea628f9

Browse files
committed
[test/refactor] 測試/簡化
1.新增測試情境 2.簡化底層代碼
1 parent 6948aeb commit ea628f9

File tree

3 files changed

+136
-93
lines changed

3 files changed

+136
-93
lines changed

src/ImmutableBase.php

Lines changed: 102 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,9 @@ final class Entity
4545
final class ArrayOf
4646
{
4747
public bool $error = false;
48-
public function __construct(
49-
public string $class = '',
50-
) {
51-
if (trim($this->class) === '') {
52-
$this->error = true;
53-
}
48+
public function __construct(string $class = '')
49+
{
50+
$this->error = empty(trim($class));
5451
}
5552
}
5653

@@ -67,7 +64,7 @@ public function __construct(array $data = [])
6764
/** @var \ReflectionNamedType|\ReflectionUnionType $type */
6865
$type = $property->getType();
6966
$exists = array_key_exists($key, $data);
70-
$isNull = !isset($data[$key]) ? true : $data[$key] === null;
67+
$isNull = !isset($data[$key]) || $data[$key] === null;
7168
$notExistsOrIsNull = !$exists || $isNull;
7269
$nullable = $type->allowsNull();
7370
$hasDefault = $property->hasDefaultValue();
@@ -78,50 +75,52 @@ public function __construct(array $data = [])
7875
throw new Exception('ArrayOf class 不能為空');
7976
}
8077
$arg = $arrayOf[0]->getArguments()[0];
81-
if (!enum_exists($arg) && !is_subclass_of($arg, self::class)) {
78+
if (!is_subclass_of($arg, self::class)) {
8279
throw new Exception('ArrayOf 指定的 class 必須為 ImmutableBase 的子類');
8380
}
8481
}
85-
$value = match(true) {
86-
$notExistsOrIsNull && !$nullable => throw new Exception("必須傳入 $type"),
87-
$arrayOf && $arg =>
88-
match(true) {
89-
$notExistsOrIsNull && $nullable => null,
90-
$notExistsOrIsNull && !$nullable => throw new Exception("必須傳入 array 或 array<{$arg}>"),
91-
$exists => is_array($data[$key]) ?
92-
array_map(fn ($item) => match(true) {
93-
is_array($item) => new $arg($item),
94-
$item instanceof $arg => $item,
95-
default => throw new Exception("陣列內容必須是 $arg 或符合其初始化所需之結構")
96-
}, $data[$key]) : throw new Exception("必須傳入 array"),
82+
$this->propertyInitialize(
83+
$property,
84+
match(true) {
85+
$notExistsOrIsNull => match(true) {
86+
!$nullable => throw new Exception("必須傳入 $type"),
87+
$nullable => $hasDefault ? $property->getDefaultValue() : null,
9788
},
98-
$notExistsOrIsNull && $nullable && !$hasDefault => null,
99-
$notExistsOrIsNull && $nullable && $hasDefault => $property->getDefaultValue(),
100-
$exists => $this->valueDecide($type, $data[$key]),
101-
};
102-
$declaring = $property->getDeclaringClass()->getName();
103-
if ($declaring !== $this::class && $property->isReadOnly()) {
104-
if ($property->isInitialized($this)) {
105-
return;
89+
$arrayOf && $arg =>
90+
match(true) {
91+
$notExistsOrIsNull => throw new Exception("必須傳入 array 或 array<{$arg}>"),
92+
is_array($data[$key]) =>
93+
array_map(fn ($item) => match(true) {
94+
is_array($item) => new $arg($item),
95+
$item instanceof $arg => $item,
96+
default => throw new Exception("陣列內容必須是 $arg 或符合其初始化所需之結構")
97+
}, $data[$key]),
98+
default => throw new Exception("必須傳入 array"),
99+
},
100+
$exists => $this->valueDecide($type, $data[$key]),
106101
}
107-
$assign = self::$classBoundSetter[$declaring] ??= Closure::bind(
108-
function (object $obj, string $prop, mixed $val): void {
109-
$obj->$prop = $val;
110-
},
111-
null,
112-
$declaring
113-
);
114-
$assign($this, $property->getName(), $value);
115-
} else {
116-
$property->setValue($this, $value);
117-
}
102+
);
118103
} catch (Exception $e) {
119-
if ($msg = $e->getMessage()) {
120-
throw new Exception("$key $msg");
121-
}
104+
throw new Exception("$key {$e->getMessage()}");
122105
}
123106
});
124107
}
108+
private function propertyInitialize(\ReflectionProperty $property, mixed $value): void
109+
{
110+
$declaring = $property->getDeclaringClass()->getName();
111+
if ($declaring !== $this::class && $property->isReadOnly()) {
112+
if ($property->isInitialized($this)) {
113+
return;
114+
}
115+
(self::$classBoundSetter[$declaring] ??= Closure::bind(
116+
fn (object $obj, string $prop, mixed $val) => $obj->$prop = $val,
117+
null,
118+
$declaring
119+
))($this, $property->getName(), $value);
120+
} else {
121+
$property->setValue($this, $value);
122+
}
123+
}
125124
private static function getReflection(object $obj): ReflectionClass
126125
{
127126
return self::$reflectionsCache[static::class] ??= new ReflectionClass($obj);
@@ -135,7 +134,7 @@ private function walkProperties(callable $callback): void
135134
{
136135
$ref = self::getReflection($this);
137136
$attrs = array_map(fn ($attr) => $attr->getName(), $ref->getAttributes());
138-
$set = array_fill_keys($attrs, true);
137+
$set = array_flip($attrs);
139138
$mode = match (true) {
140139
isset($set[DataTransferObject::class]) => 1,
141140
isset($set[ValueObject::class]) => 2,
@@ -148,21 +147,18 @@ private function walkProperties(callable $callback): void
148147
}
149148
$chain = array_reverse($chain);
150149
foreach ($chain as $cls) {
151-
foreach ($cls->getProperties() as $p) {
150+
$properties = $cls->getProperties();
151+
array_filter($properties, fn ($p) => $p->getName() === $cls->getName() || $cls->getName() !== self::class);
152+
foreach ($properties as $p) {
152153
$isPublic = $p->isPublic();
153154
$propertyName = $p->getName();
154155
$className = $p->getDeclaringClass()->getName();
155-
if ($className !== $cls->getName() || $className === self::class) {
156-
continue;
157-
}
158-
if ($mode === 2 || $mode === 3) {
159-
if ($isPublic) {
160-
throw new Exception("$className $propertyName 不允許為 public");
161-
}
162-
} else {
156+
if ($mode === 1) {
163157
if (!$isPublic || !$p->isReadOnly()) {
164158
throw new Exception("$className $propertyName 必須為 public 且 readonly");
165159
}
160+
} elseif ($isPublic) {
161+
throw new Exception("$className $propertyName 不允許為 public");
166162
}
167163
$callback($p);
168164
}
@@ -182,7 +178,7 @@ final public function with(array $data): static
182178
try {
183179
$name = $property->getName();
184180
$type = $property->getType();
185-
$newData[$name] = in_array($name, array_keys($data)) ?
181+
$newData[$name] = array_key_exists($name, $data) ?
186182
$this->valueDecide($type, $data[$name]) :
187183
$property->getValue($this);
188184
} catch (Exception $e) {
@@ -208,57 +204,73 @@ final public function toArray(): array
208204
}
209205
private function toArrayOrValue(mixed $value)
210206
{
211-
return is_object($value) && method_exists($value, 'toArray') ? $value->toArray() : $value;
207+
if (is_object($value)) {
208+
if (method_exists($value, 'toArray')) {
209+
return $value->toArray();
210+
}
211+
}
212+
return $value;
212213
}
213214
private function valueDecide(ReflectionNamedType|ReflectionUnionType $type, mixed $value): mixed
214215
{
215216
if ($type instanceof ReflectionUnionType) {
216-
$names = array_map(fn ($e) => $e->getName(), $type->getTypes());
217-
if (!in_array('array', $names, true) && is_array($value)) {
218-
throw new Exception('型別為複合且不包含array,須傳入已實例化的物件。');
219-
}
220-
foreach ($type->getTypes() as $t) {
221-
try {
222-
return $this->valueDecide($t, $value);
223-
} catch (Exception) {
224-
}
225-
}
226-
$excepts = implode('|', $names);
227-
$valueType = is_object($value) ? get_class($value) : gettype($value);
228-
throw new Exception("型別錯誤,期望:{$excepts},傳入:{$valueType}");
217+
return $this->unionTypeDecide($type, $value);
229218
} else {
230219
if (!$type->isBuiltin()) {
231-
$class = $type->getName();
232-
$value = match(true) {
233-
is_array($value) && is_subclass_of($class, self::class) => new $class($value),
234-
is_object($value) => $value,
235-
$type->allowsNull() && $value === null => null,
236-
is_string($value) && enum_exists($class) => (function () use ($class, $value) {
237-
try {
238-
return $class::tryFrom($value) ?? constant("$class::$value");
239-
} catch (Throwable) {
240-
throw new Exception("$value 不是 $class 的期望值");
241-
}
242-
})(),
243-
default => throw new Exception("型別錯誤,期望:{$class},傳入:" . (is_object($value) ? get_class($value) : gettype($value)))
244-
};
245-
} elseif ($this->builtinTypeValidate($value, $type->getName()) === false) {
246-
if ($type->allowsNull() && $value === null) {
247-
return null;
248-
} else {
249-
throw new Exception("型別錯誤,期望:{$type->getName()},傳入:".(is_object($value) ? get_class($value) : gettype($value)));
250-
}
220+
return $this->namedTypeDecide($type, $value);
221+
} elseif (
222+
$this->builtinTypeValidate($value, $type->getName()) === false &&
223+
!$this->validNullValue($type, $value)
224+
) {
225+
throw new Exception("型別錯誤,期望:{$type->getName()},傳入:".(is_object($value) ? get_class($value) : gettype($value)));
251226
}
252227
}
253228
return $value;
254229
}
230+
private function unionTypeDecide(ReflectionUnionType $type, mixed $value)
231+
{
232+
$names = array_map(fn ($e) => $e->getName(), $type->getTypes());
233+
if (!in_array('array', $names, true) && is_array($value)) {
234+
throw new Exception('型別為複合且不包含array,須傳入已實例化的物件。');
235+
}
236+
foreach ($type->getTypes() as $t) {
237+
try {
238+
return $this->valueDecide($t, $value);
239+
} catch (Exception) {
240+
}
241+
}
242+
$excepts = implode('|', $names);
243+
$valueType = is_object($value) ? get_class($value) : gettype($value);
244+
throw new Exception("型別錯誤,期望:{$excepts},傳入:{$valueType}");
245+
}
246+
private function namedTypeDecide(ReflectionNamedType $type, mixed $value)
247+
{
248+
$class = $type->getName();
249+
return match(true) {
250+
is_array($value) && is_subclass_of($class, self::class) => new $class($value),
251+
is_object($value) => $value,
252+
$this->validNullValue($type, $value) => null,
253+
is_string($value) && enum_exists($class) => (function () use ($class, $value) {
254+
try {
255+
return $class::tryFrom($value) ?? constant("$class::$value");
256+
} catch (Throwable) {
257+
throw new Exception("$value 不是 $class 的期望值");
258+
}
259+
})(),
260+
default => throw new Exception("型別錯誤,期望:{$class},傳入:" . (is_object($value) ? get_class($value) : gettype($value)))
261+
};
262+
}
263+
private function validNullValue(ReflectionNamedType $type, $value)
264+
{
265+
return $type->allowsNull() && $value === null;
266+
}
255267
private function builtinTypeValidate(mixed $value, string $type): bool
256268
{
257269
return match ($type) {
258-
'int', 'integer' => is_int($value),
259-
'float', 'double' => is_float($value),
270+
'int' => is_int($value),
271+
'float' => is_float($value),
260272
'string' => is_string($value),
261-
'bool', 'boolean' => is_bool($value),
273+
'bool' => is_bool($value),
262274
'array' => is_array($value),
263275
'object' => is_object($value),
264276
'null' => $value === null,

tests/DataTransferObjects/Advanced.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ class Advanced extends Basic
1212
/** @var Basic[] */
1313
#[ArrayOf(Basic::class)]
1414
public readonly array $arrayOfBasics;
15-
public readonly string|int $union;
15+
public readonly string|int|Basic $union;
1616
public readonly null|string|int $unionNullable;
1717
}

tests/defaultTest.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,23 @@ public function testAdvancedByInstanceWith()
320320
$modified->basic->object
321321
);
322322
}
323+
/**
324+
* 驗證 Union 包含非內建型別 toArray() 與修改資料一致。
325+
*/
326+
public function testAdvancedByArrayWithUnionToNotBuiltin()
327+
{
328+
$basic = new Basic($this->basicData);
329+
$advanced = new Advanced($this->modifyAdvancedDataByArray);
330+
$modified = $advanced->with(['union' => $basic]);
331+
$modifiedArray = $modified->toArray();
332+
$this->assertEquals(
333+
array_merge(
334+
$this->modifyAdvancedDataByArray,
335+
['union' => $basic->toArray()]
336+
),
337+
$modifiedArray
338+
);
339+
}
323340
/**
324341
* 驗證 Basic->with()->toArray() 與修改資料一致。
325342
*/
@@ -459,7 +476,7 @@ public function testValueTypeNotInUnionTypeThrowException()
459476
{
460477
$advanced = new Advanced($this->advancedDataByArray);
461478
$this->expectException(Exception::class);
462-
$this->expectExceptionMessage('union 型別錯誤,期望:string|int,傳入:double');
479+
$this->expectExceptionMessage('union 型別錯誤,期望:Tests\DataTransferObjects\Basic|string|int,傳入:double');
463480
$advanced->with(['union' => 1.1]);
464481
}
465482
/**
@@ -528,7 +545,7 @@ public function testPropertyShouldBePublicThrowException()
528545
};
529546
}
530547
/**
531-
* 驗證 ValueObject 屬性為 public 時應拋出 Exception。
548+
* 驗證 ValueObject 屬性為 public 時拋出 Exception。
532549
*/
533550
public function testPropertyShouldNotBePublicThrowException()
534551
{
@@ -538,4 +555,18 @@ public function testPropertyShouldNotBePublicThrowException()
538555
public string $string;
539556
};
540557
}
558+
559+
/**
560+
* 驗證 Union 嘗試歷遍後無吻合型別拋出 Exception。
561+
*/
562+
public function testUnionTypesSkipTilCorrectThrowException()
563+
{
564+
$this->expectException(Exception::class);
565+
new Advanced(
566+
array_merge(
567+
$this->modifyAdvancedDataByArray,
568+
['union' => 1.1]
569+
)
570+
);
571+
}
541572
}

0 commit comments

Comments
 (0)