Skip to content

Commit 8d8b6cc

Browse files
committed
[core] v4.3.0 - See CHANGELOG.md for details
1 parent 9cc1296 commit 8d8b6cc

38 files changed

Lines changed: 1784 additions & 242 deletions

CHANGELOG.md

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

3+
## [4.3.0] - 2026-04-19
4+
5+
### Added
6+
7+
- **`#[InputKeyTo(KeyCase::X)]`** — Input key case conversion. Apply at class level to remap all incoming array keys before hydration, or at property level to override for a specific property. Conversion splits on camelCase/PascalCase boundaries, underscores, hyphens, and whitespace, then rejoins in the target case.
8+
- **`#[OutputKeyTo(KeyCase::X)]`** — Output key case conversion. Apply at class level to remap all serialized keys during `toArray(true)` / `toJson(true)`, or at property level to override for a specific property.
9+
- **`KeyCase` enum** — 8 naming conventions: `Snake` (`nick_name`), `PascalSnake` (`Nick_Name`), `Macro` (`NICK_NAME`), `Camel` (`nickName`), `Pascal` (`NickName`), `Kebab` (`nick-name`), `CamelKebab` (`nick-Name`), `Train` (`Nick-Name`).
10+
- **`InvalidKeyCaseException`** — Thrown at definition time when `#[InputKeyTo]` or `#[OutputKeyTo]` receives a value that is not a `KeyCase` enum instance (e.g. a plain string).
11+
312
## [4.2.2] - 2026-03-14
413

514
### Fixed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,60 @@ try {
603603

604604
By default, the VO and SVO validation chain walks from the top of the inheritance chain down to the current class. With `#[ValidateFromSelf]` applied, the chain is reversed to start from the current class and walk upward.
605605

606+
### `#[InputKeyTo]` - Input Key Case Conversion
607+
608+
Converts incoming array keys to the specified `KeyCase` naming convention before hydration. Applied at class level, it remaps all keys; applied at property level, it overrides the class-level conversion for that property only.
609+
610+
```php
611+
use ReallifeKip\ImmutableBase\Attributes\InputKeyTo;
612+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
613+
614+
// Class-level: accepts snake_case input keys (nick_name → nickName)
615+
#[InputKeyTo(KeyCase::Camel)]
616+
readonly class UserDTO extends DataTransferObject
617+
{
618+
public string $nickName;
619+
}
620+
621+
UserDTO::fromArray(['nick_name' => 'Kip']); // nickName = 'Kip'
622+
```
623+
624+
### `#[OutputKeyTo]` - Output Key Case Conversion
625+
626+
Converts property names to the specified `KeyCase` naming convention during serialization. Applied at class level, it remaps all serialized keys; applied at property level, it overrides the class-level conversion for that property only.
627+
628+
The argument passed to `toArray()` / `toJson()` controls the conversion behavior:
629+
- `false` (default): no key conversion — property names are output as-is
630+
- `true`: applies the `#[OutputKeyTo]`-defined conversion for the current level only — nested objects use their own `#[OutputKeyTo]` declarations independently and are not affected
631+
- `KeyCase::*`: ignores `#[OutputKeyTo]` and forces the specified case globally
632+
633+
```php
634+
use ReallifeKip\ImmutableBase\Attributes\OutputKeyTo;
635+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
636+
637+
// Class-level: serializes nickName → nick_name
638+
#[OutputKeyTo(KeyCase::Snake)]
639+
readonly class UserDTO extends DataTransferObject
640+
{
641+
public string $nickName;
642+
}
643+
644+
UserDTO::fromArray(['nickName' => 'Kip'])->toArray(true); // ['nick_name' => 'Kip']
645+
```
646+
647+
Available `KeyCase` values:
648+
649+
| Case | Example |
650+
|---|---|
651+
| `KeyCase::Snake` | `nick_name` |
652+
| `KeyCase::PascalSnake` | `Nick_Name` |
653+
| `KeyCase::Macro` | `NICK_NAME` |
654+
| `KeyCase::Camel` | `nickName` |
655+
| `KeyCase::Pascal` | `NickName` |
656+
| `KeyCase::Kebab` | `nick-name` |
657+
| `KeyCase::CamelKebab` | `nick-Name` |
658+
| `KeyCase::Train` | `Nick-Name` |
659+
606660
---
607661

608662
## Configuration
@@ -681,6 +735,8 @@ Thrown when class structure or attribute configuration is incorrect. These are p
681735

682736
`InvalidSpecException` - `#[Spec]` is used without an argument or with an empty argument.
683737

738+
`InvalidKeyCaseException` - `#[InputKeyTo]` or `#[OutputKeyTo]` received a value that is not a `KeyCase` enum instance (e.g. a plain string instead of `KeyCase::Camel`).
739+
684740
`InvalidCompareTargetException` - The `equals()` comparison target is not the same class, or an array contains a non-ImmutableBase object that cannot be compared.
685741

686742
`InvalidWithPathException` - A `with()` deep path targets a scalar property that cannot be traversed further.

README_TW.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,60 @@ try {
602602

603603
VO、SVO 驗證鏈預設由繼承鏈頂層向下驗證到當前類,套用 `#[ValidateFromSelf]` 後,驗證鏈將改為從當前類開始向上驗證。
604604

605+
### `#[InputKeyTo]` - 輸入 Key 命名轉換
606+
607+
在注入前將輸入陣列的 key 轉換為指定的 `KeyCase` 命名規則。套用於類層級時,轉換所有 key;套用於屬性層級時,僅覆蓋該屬性的類層級設定。
608+
609+
```php
610+
use ReallifeKip\ImmutableBase\Attributes\InputKeyTo;
611+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
612+
613+
// 類層級:接受 snake_case 輸入 key(nick_name → nickName)
614+
#[InputKeyTo(KeyCase::Camel)]
615+
readonly class UserDTO extends DataTransferObject
616+
{
617+
public string $nickName;
618+
}
619+
620+
UserDTO::fromArray(['nick_name' => 'Kip']); // nickName = 'Kip'
621+
```
622+
623+
### `#[OutputKeyTo]` - 輸出 Key 命名轉換
624+
625+
序列化時,將屬性名稱轉換為指定的 `KeyCase` 命名規則。套用於類層級時,轉換所有序列化 key;套用於屬性層級時,僅覆蓋該屬性的類層級設定。
626+
627+
`toArray()` / `toJson()` 的引數決定轉換行為:
628+
- `false`(預設):不套用任何 key 轉換,屬性名稱原樣輸出
629+
- `true`:套用 `#[OutputKeyTo]` 定義的轉換,僅作用於當前層級,不向下滲透至巢狀物件(巢狀物件依各自的 `#[OutputKeyTo]` 宣告獨立運作)
630+
- `KeyCase::*`:忽略 `#[OutputKeyTo]`,以指定的 `KeyCase` 強制覆蓋所有 key
631+
632+
```php
633+
use ReallifeKip\ImmutableBase\Attributes\OutputKeyTo;
634+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
635+
636+
// 類層級:將 nickName 序列化為 nick_name
637+
#[OutputKeyTo(KeyCase::Snake)]
638+
readonly class UserDTO extends DataTransferObject
639+
{
640+
public string $nickName;
641+
}
642+
643+
UserDTO::fromArray(['nickName' => 'Kip'])->toArray(true); // ['nick_name' => 'Kip']
644+
```
645+
646+
可用的 `KeyCase` 值:
647+
648+
| 命名規則 | 範例 |
649+
|---|---|
650+
| `KeyCase::Snake` | `nick_name` |
651+
| `KeyCase::PascalSnake` | `Nick_Name` |
652+
| `KeyCase::Macro` | `NICK_NAME` |
653+
| `KeyCase::Camel` | `nickName` |
654+
| `KeyCase::Pascal` | `NickName` |
655+
| `KeyCase::Kebab` | `nick-name` |
656+
| `KeyCase::CamelKebab` | `nick-Name` |
657+
| `KeyCase::Train` | `Nick-Name` |
658+
605659
---
606660

607661
## 設定
@@ -680,6 +734,8 @@ vendor/bin/ib-writer
680734

681735
`InvalidSpecException` - `#[Spec]` 未提供引數或引數為空。
682736

737+
`InvalidKeyCaseException` - `#[InputKeyTo]``#[OutputKeyTo]` 接收到非 `KeyCase` enum 實例的值(例如傳入純字串而非 `KeyCase::Camel`)。
738+
683739
`InvalidCompareTargetException` - `equals()` 的比較對象與自身類不同,或陣列中包含無法比較的非 ImmutableBase 物件。
684740

685741
`InvalidWithPathException` - `with()` 的深層路徑指向純量屬性,無法向下展開。
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
namespace Benchmarks\DataTransferObjects;
5+
6+
use ReallifeKip\ImmutableBase\Attributes\InputKeyTo;
7+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
8+
use ReallifeKip\ImmutableBase\Objects\DataTransferObject;
9+
10+
#[InputKeyTo(KeyCase::Camel)]
11+
readonly class InputKeyDTO extends DataTransferObject
12+
{
13+
public string $firstName;
14+
public string $lastName;
15+
public string $emailAddress;
16+
public int $accountId;
17+
public bool $isActive;
18+
public string $createdAt;
19+
public string $updatedAt;
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
namespace Benchmarks\DataTransferObjects;
5+
6+
use ReallifeKip\ImmutableBase\Attributes\OutputKeyTo;
7+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
8+
use ReallifeKip\ImmutableBase\Objects\DataTransferObject;
9+
10+
#[OutputKeyTo(KeyCase::Snake)]
11+
readonly class OutputKeyDTO extends DataTransferObject
12+
{
13+
public string $firstName;
14+
public string $lastName;
15+
public string $emailAddress;
16+
public int $accountId;
17+
public bool $isActive;
18+
public string $createdAt;
19+
public string $updatedAt;
20+
}

benchmarks/DefaultBench.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
use Benchmarks\DataTransferObjects\Order;
88
use Benchmarks\DataTransferObjects\SimpleDTO;
99
use ReallifeKip\ImmutableBase\ImmutableBase;
10-
use ReallifeKip\ImmutableBase\StaticStatus;
1110

12-
if (StaticStatus::$cachedMeta === []) {
11+
if (ImmutableBase::state()['cachedMeta'] === []) {
1312
ImmutableBase::loadCache();
1413
}
1514

benchmarks/KeyCaseBench.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
namespace Benchmarks;
5+
6+
use Benchmarks\DataTransferObjects\InputKeyDTO;
7+
use Benchmarks\DataTransferObjects\OutputKeyDTO;
8+
use ReallifeKip\ImmutableBase\ImmutableBase;
9+
10+
if (ImmutableBase::state()['cachedMeta'] === []) {
11+
ImmutableBase::loadCache();
12+
}
13+
14+
/**
15+
* @Warmup(5)
16+
* @BeforeMethods("setUp")
17+
* @Revs(1000)
18+
* @Iterations(15)
19+
* @OutputTimeUnit("milliseconds")
20+
*/
21+
class KeyCaseBench
22+
{
23+
private array $snakePayload;
24+
private array $camelPayload;
25+
private OutputKeyDTO $outputKeyDTO;
26+
27+
public function setUp()
28+
{
29+
$this->snakePayload = [
30+
'first_name' => 'Kip',
31+
'last_name' => 'The Architect',
32+
'email_address' => 'kip@example.com',
33+
'account_id' => 42,
34+
'is_active' => true,
35+
'created_at' => '2026-01-01T00:00:00Z',
36+
'updated_at' => '2026-04-19T00:00:00Z',
37+
];
38+
39+
$this->camelPayload = [
40+
'firstName' => 'Kip',
41+
'lastName' => 'The Architect',
42+
'emailAddress' => 'kip@example.com',
43+
'accountId' => 42,
44+
'isActive' => true,
45+
'createdAt' => '2026-01-01T00:00:00Z',
46+
'updatedAt' => '2026-04-19T00:00:00Z',
47+
];
48+
49+
$this->outputKeyDTO = OutputKeyDTO::fromArray($this->camelPayload);
50+
}
51+
52+
public function benchInputKeyHydration(): void
53+
{
54+
InputKeyDTO::fromArray($this->snakePayload);
55+
}
56+
57+
public function benchOutputKeySerialization(): void
58+
{
59+
$this->outputKeyDTO->toArray(true);
60+
}
61+
62+
public function benchRoundTrip(): void
63+
{
64+
OutputKeyDTO::fromArray($this->camelPayload)->toArray(true);
65+
}
66+
}

benchmarks/WidePayloadBench.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
use Benchmarks\DataTransferObjects\TargetDTO;
77
use Benchmarks\DataTransferObjects\WideFlatDTO;
88
use ReallifeKip\ImmutableBase\ImmutableBase;
9-
use ReallifeKip\ImmutableBase\StaticStatus;
109

11-
if (StaticStatus::$cachedMeta === []) {
10+
if (ImmutableBase::state()['cachedMeta'] === []) {
1211
ImmutableBase::loadCache();
1312
}
1413

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
use Benchmarks\Enums\OrderStatus;
1111
use PhpBench\Attributes\BeforeMethods;
1212
use ReallifeKip\ImmutableBase\ImmutableBase;
13-
use ReallifeKip\ImmutableBase\StaticStatus;
1413

15-
if (StaticStatus::$cachedMeta === []) {
14+
if (ImmutableBase::state()['cachedMeta'] === []) {
1615
ImmutableBase::loadCache();
1716
}
1817

src/Attributes/InputKeyTo.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
5+
namespace ReallifeKip\ImmutableBase\Attributes;
6+
7+
use ReallifeKip\ImmutableBase\Enums\KeyCase;
8+
9+
/**
10+
* Converts input array keys to a specified naming convention before hydration.
11+
*
12+
* When applied to a class, all input keys are converted to the target case
13+
* before being matched against property names.
14+
*
15+
* When applied to a property, overrides any class-level conversion for that
16+
* specific property only.
17+
*
18+
* Conversion splits words on camelCase/PascalCase boundaries, underscores,
19+
* hyphens, and whitespace, then rejoins in the target case.
20+
*
21+
* @example #[InputKeyTo(KeyCase::Camel)] — accepts snake_case input keys
22+
* @example #[InputKeyTo(KeyCase::Snake)] — accepts camelCase input keys
23+
*/
24+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY)]
25+
final class InputKeyTo
26+
{
27+
private function __construct(KeyCase $keyCase)
28+
{
29+
// Prevents manual instantiation of the attribute.
30+
}
31+
}

0 commit comments

Comments
 (0)