Skip to content

Commit 9292f91

Browse files
committed
[core] v4.1.0 - See CHANGELOG.md for details
1 parent 34324d9 commit 9292f91

22 files changed

Lines changed: 849 additions & 52 deletions

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# CHANGELOG
22

3+
## [v4.1.0] - 2026-03-07
4+
5+
### Added
6+
7+
- **`defaultValues()` — Static default value declaration.** Override `defaultValues(): array` to declare fallback values for properties absent from input data. Keys must match declared property names; unmatched keys are silently ignored. Supports any type valid for the target property, including subclasses of ImmutableBase and Enum.
8+
- **`#[Defaults(value)]` — Attribute-based default value.** Apply `#[Defaults(value)]` to individual properties for constant-expression defaults. Constrained by PHP attribute syntax to scalar values, arrays, and class constants.
9+
- **Default value resolution priority.** During construction (`fromArray` / `fromJson`), property values are resolved in the following order:
10+
1. Explicit input value (including explicit `null`)
11+
2. `defaultValues()[$propertyName]`
12+
3. `#[Defaults(value)]` attribute value
13+
4. `null` (if nullable) or `RequiredValueException`
14+
- **Explicit `null` is respected.** When a key is present in the input with a `null` value, it is treated as an intentional assignment — default values are not applied.
15+
- **Cache-aware default values.** `ib-cacher` serializes cacheable default values (scalars, arrays) into the cache file. Non-serializable defaults (objects, Closures) are excluded from the cache with a `[Notice]` warning and resolved at runtime via `defaultValues()` on every construction.
16+
- **SVO `defaultValues()` sealed.** `SingleValueObject::defaultValues()` is declared `final` and returns an empty array. SVOs require explicit values via `from()` by design.
17+
18+
### Changed
19+
20+
- **`__construct()` uses `array_key_exists()` for default resolution.** Replaces `isset()` to correctly distinguish between "key absent" (apply default) and "key present with `null`" (respect explicit null).
21+
322
## [v4.0.0] - 2026-03-01
423

524
### Breaking Changes

README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ class Order extends DataTransferObject
5151
new Order('2026-01-01', '00:00:00', ...); // Cannot directly accept external array or JSON data, and risks argument misordering if parameter names are not explicitly specified
5252
```
5353

54+
### 🛡️ Declarative Default Values
55+
```php
56+
// 🥳 ImmutableBase fills missing properties from defaultValues() or #[Defaults], with clear priority and null-awareness.
57+
readonly class CreateUserDTO extends DataTransferObject
58+
{
59+
public string $name;
60+
#[Defaults('member')]
61+
public string $role;
62+
63+
public static function defaultValues(): array
64+
{
65+
return ['role' => 'admin']; // Takes precedence over #[Defaults]
66+
}
67+
}
68+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'admin'
69+
70+
// 🫤 The conventional approach requires manual null-coalescing or constructor defaults, with no centralized declaration.
71+
class CreateUserDTO {
72+
public function __construct(
73+
public readonly string $name,
74+
public readonly string $role = 'member', // Cannot be overridden per-class without rewriting constructors
75+
){}
76+
}
77+
```
78+
5479
### 🔧 Flexible Deep Path Updates
5580
Update deeply nested properties by path - no Russian nesting dolls.
5681
```php
@@ -355,8 +380,107 @@ $age1->equals($age3); // false
355380

356381
---
357382

383+
## Default Values
384+
385+
Properties absent from input data can be populated with fallback values via two complementary mechanisms.
386+
387+
### `defaultValues()` — Dynamic Defaults
388+
389+
Override the static method to declare default values as an associative array keyed by property name. Supports any type valid for the target property, including subclasses of ImmutableBase and Enum.
390+
391+
```php
392+
readonly class CreateUserDTO extends DataTransferObject
393+
{
394+
public string $name;
395+
public string $role;
396+
public string $locale;
397+
398+
public static function defaultValues(): array
399+
{
400+
return [
401+
'role' => 'member',
402+
'locale' => 'en',
403+
];
404+
}
405+
}
406+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member', locale = 'en'
407+
```
408+
409+
### `#[Defaults]` — Attribute Defaults
410+
411+
Apply `#[Defaults(value)]` to individual properties for inline constant-expression defaults. Constrained by PHP attribute syntax to scalars, arrays, and class constants.
412+
413+
```php
414+
use ReallifeKip\ImmutableBase\Attributes\Defaults;
415+
416+
readonly class CreateUserDTO extends DataTransferObject
417+
{
418+
public string $name;
419+
#[Defaults('member')]
420+
public string $role;
421+
#[Defaults('en')]
422+
public string $locale;
423+
}
424+
```
425+
426+
### Resolution Priority
427+
428+
When a property key is absent from the input data, defaults are resolved in this order:
429+
430+
1. `defaultValues()[$propertyName]`
431+
2. `#[Defaults(value)]` attribute value
432+
3. `null` (if nullable) or `RequiredValueException`
433+
434+
When both mechanisms define a value for the same property, `defaultValues()` takes precedence.
435+
436+
### Explicit `null` Is Not Absent
437+
438+
When a key is present in the input with a `null` value, it is treated as an intentional assignment — default values are **not** applied.
439+
440+
```php
441+
readonly class Config extends DataTransferObject
442+
{
443+
public ?string $theme;
444+
445+
public static function defaultValues(): array
446+
{
447+
return ['theme' => 'dark'];
448+
}
449+
}
450+
451+
Config::fromArray([]); // theme = 'dark' (key absent → default)
452+
Config::fromArray(['theme' => null]); // theme = null (explicit null → respected)
453+
Config::fromArray(['theme' => 'light']); // theme = 'light' (explicit value → used)
454+
```
455+
456+
### Caching Behavior
457+
458+
`ib-cacher` serializes cacheable default values (scalars, arrays) into the cache file. Non-serializable values (objects, Closures, resources) are excluded with a `[Notice]` warning and resolved at runtime via `defaultValues()` instead.
459+
460+
### SVO Restriction
461+
462+
`SingleValueObject` does not support default values. SVOs require an explicit value via `from()` by design. The `defaultValues()` method is sealed (`final`) on `SingleValueObject` and always returns an empty array.
463+
464+
---
465+
358466
## Attributes
359467

468+
### `#[Defaults]` - Property Default Value
469+
470+
Declares a fallback value for a single property when the key is absent from input data. Constrained by PHP attribute syntax to scalar values, arrays, and class constants. For dynamic or object defaults, use `defaultValues()` instead.
471+
472+
```php
473+
use ReallifeKip\ImmutableBase\Attributes\Defaults;
474+
475+
readonly class CreateUserDTO extends DataTransferObject
476+
{
477+
public string $name;
478+
#[Defaults('member')]
479+
public string $role;
480+
}
481+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'
482+
```
483+
360484
### `#[ArrayOf]` - Typed Array
361485

362486
Marks an array property as a typed collection of ImmutableBase instances. Each element is automatically instantiated from arrays, JSON strings, or pre-built objects. The target class must be a subclass of DTO, VO, or SVO.

README_TW.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ class Order extends DataTransferObject
5151
new Order('2026-01-01', '00:00:00', ...); // 無法直接接受外部傳入的陣列或 JSON 資料,且若未明確指定參數名稱則有順序錯亂的風險
5252
```
5353

54+
### 🛡️ 宣告式預設值
55+
```php
56+
// 🥳 ImmutableBase 透過 defaultValues() 或 #[Defaults] 自動填充缺少的屬性,優先順序清晰且能正確區分 null。
57+
readonly class CreateUserDTO extends DataTransferObject
58+
{
59+
public string $name;
60+
#[Defaults('member')]
61+
public string $role;
62+
63+
public static function defaultValues(): array
64+
{
65+
return ['role' => 'admin']; // 優先於 #[Defaults]
66+
}
67+
}
68+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'admin'
69+
70+
// 🫤 一般常見做法需要手動 null 合併或建構子預設值,無法集中宣告。
71+
class CreateUserDTO {
72+
public function __construct(
73+
public readonly string $name,
74+
public readonly string $role = 'member', // 無法在子類中覆寫,除非重寫整個建構子
75+
){}
76+
}
77+
```
78+
5479
### 🔧靈活便利的深層更新
5580
支援直接指定物件深層路徑,拒絕俄羅斯套娃。
5681
```php
@@ -355,8 +380,107 @@ $age1->equals($age3); // false
355380

356381
---
357382

383+
## 預設值
384+
385+
輸入資料中缺少的屬性可透過兩種互補機制自動填充預設值。
386+
387+
### `defaultValues()` — 動態預設值
388+
389+
覆寫靜態方法,以屬性名稱為索引的關聯陣列宣告預設值,支援任何符合目標屬性型別的值,包含 ImmutableBase 的子物件及 Enum。
390+
391+
```php
392+
readonly class CreateUserDTO extends DataTransferObject
393+
{
394+
public string $name;
395+
public string $role;
396+
public string $locale;
397+
398+
public static function defaultValues(): array
399+
{
400+
return [
401+
'role' => 'member',
402+
'locale' => 'en',
403+
];
404+
}
405+
}
406+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member', locale = 'en'
407+
```
408+
409+
### `#[Defaults]` — 標註預設值
410+
411+
對個別屬性套用 `#[Defaults(value)]`,以行內常量表達式宣告預設值,受 PHP 標註語法限制,僅支援純量值、陣列及類別常數。
412+
413+
```php
414+
use ReallifeKip\ImmutableBase\Attributes\Defaults;
415+
416+
readonly class CreateUserDTO extends DataTransferObject
417+
{
418+
public string $name;
419+
#[Defaults('member')]
420+
public string $role;
421+
#[Defaults('en')]
422+
public string $locale;
423+
}
424+
```
425+
426+
### 解析優先順序
427+
428+
當屬性的 key 不存在於輸入資料中時,預設值依以下順序解析:
429+
430+
1. `defaultValues()[$propertyName]`
431+
2. `#[Defaults(value)]` 標註值
432+
3. `null`(若為 nullable)或 `RequiredValueException`
433+
434+
當兩種機制同時為同一屬性定義預設值時,`defaultValues()` 優先。
435+
436+
### 明確傳入 `null` 不等於缺少
437+
438+
當 key 存在於輸入資料中但值為 `null` 時,視為使用者明確指定——**不會**套用預設值。
439+
440+
```php
441+
readonly class Config extends DataTransferObject
442+
{
443+
public ?string $theme;
444+
445+
public static function defaultValues(): array
446+
{
447+
return ['theme' => 'dark'];
448+
}
449+
}
450+
451+
Config::fromArray([]); // theme = 'dark' (key 缺少 → 套用預設值)
452+
Config::fromArray(['theme' => null]); // theme = null (明確傳入 null → 維持 null)
453+
Config::fromArray(['theme' => 'light']); // theme = 'light' (明確傳入值 → 使用傳入值)
454+
```
455+
456+
### 快取行為
457+
458+
`ib-cacher` 會將可序列化的預設值(純量、陣列)寫入快取檔案,不可序列化的值(物件、Closure、resource)會以 `[Notice]` 警告排除,改為每次建構時透過 `defaultValues()` 在執行期解析。
459+
460+
### SVO 限制
461+
462+
`SingleValueObject` 不支援預設值,SVO 在設計上要求透過 `from()` 明確傳入值,`defaultValues()``SingleValueObject` 上以 `final` 封閉,始終回傳空陣列。
463+
464+
---
465+
358466
## Attributes
359467

468+
### `#[Defaults]` - 屬性預設值
469+
470+
當 key 不存在於輸入資料中時,為單一屬性宣告預設值,受 PHP 標註語法限制,僅支援純量值、陣列及類別常數,若需動態或物件預設值,請改用 `defaultValues()`
471+
472+
```php
473+
use ReallifeKip\ImmutableBase\Attributes\Defaults;
474+
475+
readonly class CreateUserDTO extends DataTransferObject
476+
{
477+
public string $name;
478+
#[Defaults('member')]
479+
public string $role;
480+
}
481+
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'
482+
```
483+
360484
### `#[ArrayOf]` - 型別陣列
361485

362486
將陣列屬性標記為 ImmutableBase 實例的型別集合。每個元素會自動從陣列、JSON 字串或已建構物件進行實例化,目標類必須是 DTO、VO 或 SVO 的子類。

src/Attributes/Defaults.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace ReallifeKip\ImmutableBase\Attributes;
4+
5+
/**
6+
* Declares a per-property fallback value used when the input payload
7+
* does not contain that property key.
8+
*
9+
* Resolution precedence is:
10+
* 1. Explicit input value
11+
* 2. ImmutableBase::defaultValues()
12+
* 3. #[Defaults(...)]
13+
*
14+
* This attribute is ignored when the key is explicitly present with `null`.
15+
*
16+
* Note: the attribute name is `#[Defaults]` (plural). `#[Default]` is not
17+
* supported and will not be recognized.
18+
*/
19+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
20+
final class Defaults
21+
{
22+
/**
23+
* @param mixed $value Fallback value to apply when the property key is absent.
24+
*/
25+
public function __construct(
26+
public mixed $value
27+
) {}
28+
}

src/BasicTrait.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
5+
namespace ReallifeKip\ImmutableBase;
6+
7+
use ReflectionClass;
8+
use ReflectionProperty;
9+
10+
trait BasicTrait
11+
{
12+
/**
13+
* Reads constructor arguments from a reflected PHP attribute.
14+
*
15+
* Used as a lightweight helper during metadata scanning to avoid
16+
* repetitive boilerplate around `getAttributes()` + `getArguments()`.
17+
*
18+
* When `$getFirst` is true (default), returns only the first argument
19+
* of the first matching attribute instance. When false, returns the
20+
* full argument array of the first matching instance.
21+
*
22+
* If the attribute is not present, or present without constructor
23+
* arguments, `null` is returned.
24+
*
25+
* @param ReflectionClass|ReflectionProperty $target Reflection target to inspect.
26+
* @param class-string $name Fully-qualified attribute class name.
27+
* @param bool $getFirst Whether to return only the first argument.
28+
* @return mixed|null
29+
*/
30+
public static function getAttributeArgument(ReflectionClass | ReflectionProperty $target, string $name, bool $getFirst = true)
31+
{
32+
if ($value = $target->getAttributes($name)) {
33+
$value = $value[0];
34+
if ($value = $value->getArguments()) {
35+
return $getFirst ? $value[0] : $value;
36+
}
37+
38+
return null;
39+
}
40+
41+
return null;
42+
}
43+
}

0 commit comments

Comments
 (0)