🌐 其他語言版本:English
一個用於建構不可變資料物件的 PHP 函式庫,具備嚴格的型別驗證機制,適用於 DTO(資料傳輸物件)、VO(值物件) 及 SVO(單值物件)。
強調不可變性、型別安全及深層結構操作,包含巢狀建構、點路徑變更、以及遞迴的相等性比較。
// 🥳 ImmutableBase 不需要撰寫建構子,直接傳入陣列或 JSON 資料即可建構,且傳入的資料 key 無順序限制,可自由排序。
readonly class Order extends DataTransferObject
{
public string $date;
public string $time;
}
Order::fromArray($data); // $data 必須是陣列(JSON 字串請改用 fromJson())
// 🫤 一般常見做法需要重複撰寫建構子,也常因順序不正確而無法建構,且無法直接使用外部傳入資料進行建構。
class Order extends DataTransferObject
{
public function __construct(
public readonly string $date,
public readonly string $time
){}
}
new Order('2026-01-01', '00:00:00', ...); // 無法直接接受外部傳入的陣列或 JSON 資料,且若未明確指定參數名稱則有順序錯亂的風險// 🥳 ImmutableBase 透過 defaultValues() 或 #[Defaults] 自動填充缺少的屬性,優先順序清晰且能正確區分 null。
readonly class CreateUserDTO extends DataTransferObject
{
public string $name;
#[Defaults('member')]
public string $role;
public static function defaultValues(): array
{
return ['role' => 'admin']; // 優先於 #[Defaults]
}
}
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'admin'
// 🫤 一般常見做法需要手動 null 合併或建構子預設值,無法集中宣告。
class CreateUserDTO {
public function __construct(
public readonly string $name,
public readonly string $role = 'member', // 無法在子類中覆寫,除非重寫整個建構子
){}
}支援直接指定物件深層路徑,拒絕俄羅斯套娃。
// 🥳 ImmutableBase 靈活且精準。
$order->with(['items.0.count' => 1]); // 直接指定物件陣列索引並更改 count
// 🫤 一般常見做法複雜且無法保障原物件陣列的其他內容。
$order->with([
'items' => [
[
'count' => 1
]
]
])// 🥳 ImmutableBase 清楚明瞭指出錯誤位置。
SomeException: Order > $profile > 0 > $count > {錯誤訊息}
// 🫤 一般常見做法只有模糊或難以追蹤的基礎訊息。
SomeException: {錯誤訊息}🥳 ImmutableBase 可以透過 vendor/bin/ib-cacher 掃瞄並建置所有 ImmutableBase 物件快取檔案 ib-cache.php,極致優化速度。
🫤 一般常見做法可能根本不存在快取機制,每次運行都需要為反射付出大量時間成本。
🥳 ImmutableBase 的 ValueObject、SingleValueObject 可選設計 validate(): bool,使物件在建構初期就自動由繼承鏈最上層開始向下歷遍 validate(): bool 進行驗證,且可透過 #[ValidateFromSelf] 反轉驗證方向。
🫤 一般常見做法幾乎無自動驗證鏈機制及概念,只能透過建構子自己設計。
🥳 ImmutableBase 可以透過 vendor/bin/ib-writer 對專案進行 ImmutableBase 子類物件掃描,力求避免文件與代碼不一致、需要花費額外人力的窘境,快速產出 Mermaid 類別圖、Markdown 屬性表及 TypeScript 型別宣告等技術文件。
🫤 一般常見做法無法保障代碼與文件一致。
🥳 ImmutableBase 使用時,若無產出文件、產出快取、單元或效能測試的需求,不需要額外安裝任何依賴,不依附於任何框架。
🫤 一般常見做法若依賴特定套件或框架則難以快速解藕。
// 🥳 ImmutableBase 可以透過 `#[KeepOnNull]`、`#[SkipOnNull]` 標籤精準控制屬性為空時是否輸出,不需親自過濾。
#[SkipOnNull]
readonly class User extends ValueObject
{
#[KeepOnNull]
public ?string $name;
public ?int $age;
}
User::fromArray([])->toArray(); // ["name" => null]
// 🫤 一般常見做法通常需要親自手動過濾 null。
readonly class User extends ValueObject
{
public ?string $name;
public ?int $age;
}
$user = new User();
$data = get_object_vars($user);
$data['name'] ??= null;// 🥳 ImmutableBase 雖然約束了 `SingleValueObject` 必須宣告 $value,但允許靈活、自由定義該屬性型別。(透過 interface + hooked property 設計隔代約束,零反射開銷)
readonly class ValidAge extends SingleValueObject
{
public int $value; // 與物件名稱語義相符的型別
}
// 🫤 一般常見做法交由 parent 宣告型別且無法自訂,parent 通常宣告為 mixed 或複雜、太寬的聯型,難以設計 SVO。
class ValidAge extends SingleValueObject
{
public string $value; // parent 約束了型別,無法改變,與物件名稱語義不符
}composer require reallifekip/immutable-base需要 PHP 8.4 以上。
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
use ReallifeKip\ImmutableBase\Objects\DataTransferObject;
use ReallifeKip\ImmutableBase\Objects\ValueObject;
use ReallifeKip\ImmutableBase\Objects\SingleValueObject;
readonly class ValidAge extends SingleValueObject
{
public int $value;
public function validate(): bool
{
return $this->value >= 18;
}
}
readonly class User extends ValueObject
{
public string $name;
public ValidAge $age;
public function validate(): bool
{
return mb_strlen($this->name) >= 2;
}
}
readonly class SignUpUsersDTO extends DataTransferObject
{
#[ArrayOf(User::class)]
public array $users;
public int $userCount;
}
$signUp = SignUpUsersDTO::fromArray([
'users' => [
['name' => 'ReallifeKip', 'age' => 18], // 陣列
'{"name": "Bob", "age": 19}', // JSON 字串
User::fromArray(['name' => 'Carl', 'age' => 20]), // 實例 fromArray
User::fromJson('{"name": "Dave", "age": 21}'), // 實例 fromJson
],
'userCount' => 4,
]);🔗 想快速嘗試?JSON to ImmutableBase Converter 讓你貼上 JSON 就能快速產出 ImmutableBase 物件!
# 單元測試
vendor/bin/phpunit tests
# 效能測試
vendor/bin/phpbench run傳輸、交互用的純資料結構,即便設計 validate(): bool,也不會在建構過程中觸發進行驗證。
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
use ReallifeKip\ImmutableBase\Objects\DataTransferObject;
readonly class SignUpUsersDTO extends DataTransferObject
{
#[ArrayOf(User::class)]
public array $users;
public int $userCount;
}具語義的資料結構,可以透過設計函式 validate(): bool 在建構過程中自動驗證。
use ReallifeKip\ImmutableBase\Objects\ValueObject;
readonly class User extends ValueObject
{
public string $name;
public ValidAge $age;
public function validate(): bool
{
return mb_strlen($this->name) >= 2;
}
}具語義的單一資料,可以透過設計函式 validate(): bool 在建構過程中自動驗證,此類及其子類物件 validate()、from()、jsonSerialize()、__toString()、__invoke() 僅對 $value 屬性生效。
use ReallifeKip\ImmutableBase\Objects\SingleValueObject;
readonly class ValidAge extends SingleValueObject
{
public int $value;
public function validate(): bool
{
return $this->value >= 18;
}
}$age = ValidAge::from(18);
echo $age; // 18(透過 __toString,會將 $value 轉為字串)
echo $age(); // 18(透過 __invoke)
echo $age->value; // 18建構輸入的資料中,不屬於已宣告屬性的 key 會被靜默忽略(除非啟用嚴格模式)。
$user = User::fromArray(['name' => 'Kip', 'age' => 18]);
$user = User::fromJson('{"name": "Kip", "age": 18}');$age = ValidAge::from(18);$user->toArray(); // ['name' => 'ReallifeKip', 'age' => 18]
$user->toJson(); // {"name":"ReallifeKip","age":18}更新指定屬性並回傳一個新實例,原始物件不會被修改,可接受陣列、物件或 JSON 字串。
$newUser = $user->with(['name' => 'Kip']);
$newUser = $user->with('{"name": "Kip"}');
$newUser = $user->with((object) ['name' => 'Kip']);深層路徑語法 - 透過點記法、中括號記法或自訂分隔符更新巢狀屬性:
// 點記法
$newSignUp = $signUp->with(['users.0.name' => 'Kip']);
// 中括號記法
$newSignUp = $signUp->with(['users[0].name' => 'Kip']);
// 自訂分隔符
$newSignUp = $signUp->with(['users/0/name' => 'Kip'], '/');SVO with() - 直接替換封裝的值:
$newAge = $age->with(20);深層結構相等性比較。適用於所有 ImmutableBase 子類物件,比對對象的資料、結構、類需與自身完全相同,巢狀 ImmutableBase 物件及陣列會被遞迴比較。
$a = User::fromArray(['name' => 'Kip', 'age' => 18]);
$b = User::fromArray(['name' => 'Kip', 'age' => 18]);
$c = User::fromArray(['name' => 'Kip', 'age' => 20]);
$a->equals($b); // true - 相同資料,不同實例
$a->equals($c); // false - age 不同對 SVO 子類而言,直接比較封裝的 $value:
$age1 = ValidAge::from(18);
$age2 = ValidAge::from(18);
$age3 = ValidAge::from(20);
$age1->equals($age2); // true
$age1->equals($age3); // false輸入資料中缺少的屬性可透過兩種互補機制自動填充預設值。
覆寫靜態方法,以屬性名稱為索引的關聯陣列宣告預設值,支援任何符合目標屬性型別的值,包含 ImmutableBase 的子物件及 Enum。
readonly class CreateUserDTO extends DataTransferObject
{
public string $name;
public string $role;
public string $locale;
public static function defaultValues(): array
{
return [
'role' => 'member',
'locale' => 'en',
];
}
}
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member', locale = 'en'對個別屬性套用 #[Defaults(value)],以行內常量表達式宣告預設值,受 PHP 標註語法限制,僅支援純量值、陣列及類別常數。
use ReallifeKip\ImmutableBase\Attributes\Defaults;
readonly class CreateUserDTO extends DataTransferObject
{
public string $name;
#[Defaults('member')]
public string $role;
#[Defaults('en')]
public string $locale;
}當屬性的 key 不存在於輸入資料中時,預設值依以下順序解析:
defaultValues()[$propertyName]#[Defaults(value)]標註值null(若為 nullable)或RequiredValueException
當兩種機制同時為同一屬性定義預設值時,defaultValues() 優先。
當 key 存在於輸入資料中但值為 null 時,視為使用者明確指定——不會套用預設值。
readonly class Config extends DataTransferObject
{
public ?string $theme;
public static function defaultValues(): array
{
return ['theme' => 'dark'];
}
}
Config::fromArray([]); // theme = 'dark' (key 缺少 → 套用預設值)
Config::fromArray(['theme' => null]); // theme = null (明確傳入 null → 維持 null)
Config::fromArray(['theme' => 'light']); // theme = 'light' (明確傳入值 → 使用傳入值)ib-cacher 會將可序列化的預設值(純量、陣列)寫入快取檔案,不可序列化的值(物件、Closure、resource)會以 [Notice] 警告排除,改為每次建構時透過 defaultValues() 在執行期解析。
SingleValueObject 不支援預設值,SVO 在設計上要求透過 from() 明確傳入值,defaultValues() 在 SingleValueObject 上以 final 封閉,始終回傳空陣列。
當 key 不存在於輸入資料中時,為單一屬性宣告預設值,受 PHP 標註語法限制,僅支援純量值、陣列及類別常數,若需動態或物件預設值,請改用 defaultValues()。
use ReallifeKip\ImmutableBase\Attributes\Defaults;
readonly class CreateUserDTO extends DataTransferObject
{
public string $name;
#[Defaults('member')]
public string $role;
}
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'將陣列屬性標記為 ImmutableBase 實例或純量值的型別集合。每個元素會自動驗證或實例化。目標必須是 DTO、VO 或 SVO 的子類,或純量陣列可使用 Native enum case。
純量型別陣列可使用 Native enum case 取代類別名稱:
| Case | PHP 型別 |
|---|---|
Native::string |
string |
Native::int |
int |
Native::float |
float |
Native::bool |
bool |
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
readonly class SignUpUsersDTO extends DataTransferObject
{
// ImmutableBase 子類
#[ArrayOf(User::class)]
public array $users;
// 純量型別陣列
#[ArrayOf(Native::string)]
public array $tags;
#[ArrayOf(Native::int)]
public array $scores;
}拒絕不存在於已宣告屬性的 key 資料輸入。
use ReallifeKip\ImmutableBase\Attributes\Strict;
#[Strict]
readonly class User extends ValueObject
{
public string $name;
public ValidAge $age;
// ...
}
User::fromArray(['name' => 'Kip', 'age' => 18, 'extra' => '...']);
// StrictViolationException: Disallowed 'extra' for User.使類無視嚴格模式約束,接受不存在於已宣告屬性的 key 資料輸入,權重高於 #[Strict]、ImmutableBase::strict()。
use ReallifeKip\ImmutableBase\Attributes\Lax;
#[Lax]
readonly class User extends ValueObject
{
public string $name;
public ValidAge $age;
// ...
}
User::fromArray(['name' => 'Kip', 'age' => 18, 'extra' => '...']); // 正常建構#[SkipOnNull] 使 toArray()、toJson() 輸出排除值為 null 的內容,可套用於類層級(影響所有屬性)或屬性層級(僅影響單一屬性)。
#[KeepOnNull] 僅可套用於屬性層級,無視 #[SkipOnNull] 效果,使該屬性即使為 null 仍輸出。
類未使用 #[SkipOnNull] 時,toArray()、toJson() 預設會輸出值為 null 的內容。
use ReallifeKip\ImmutableBase\Attributes\SkipOnNull;
use ReallifeKip\ImmutableBase\Attributes\KeepOnNull;
#[SkipOnNull]
readonly class UserDTO extends DataTransferObject
{
#[KeepOnNull]
public ?string $name; // 即使為 null 也保留在輸出中
public ValidAge|null $age; // 為 null 時從輸出中排除
}
UserDTO::fromArray([])->toArray();
// ['name' => null](age 被排除,name 因 KeepOnNull 保留)VO、SVO 的可選附加訊息,當 validate() 回傳 false 時,此訊息會包含在 ValidationChainException 中,使用者可透過 $exception->getSpec() 取得訊息內容。
use ReallifeKip\ImmutableBase\Attributes\Spec;
use ReallifeKip\ImmutableBase\Exceptions\ValidationExceptions\ValidationChainException;
#[Spec('年齡必須大於等於 18')]
readonly class ValidAge extends SingleValueObject
{
public int $value;
public function validate(): bool
{
return $this->value >= 18;
}
}
try {
ValidAge::from(10);
} catch (ValidationChainException $e) {
echo $e->getSpec(); // 年齡必須大於等於 18
}VO、SVO 驗證鏈預設由繼承鏈頂層向下驗證到當前類,套用 #[ValidateFromSelf] 後,驗證鏈將改為從當前類開始向上驗證。
在注入前將輸入陣列的 key 轉換為指定的 KeyCase 命名規則。套用於類層級時,轉換所有 key;套用於屬性層級時,僅覆蓋該屬性的類層級設定。
use ReallifeKip\ImmutableBase\Attributes\InputKeyTo;
use ReallifeKip\ImmutableBase\Enums\KeyCase;
// 類層級:接受 snake_case 輸入 key(nick_name → nickName)
#[InputKeyTo(KeyCase::Camel)]
readonly class UserDTO extends DataTransferObject
{
public string $nickName;
}
UserDTO::fromArray(['nick_name' => 'Kip']); // nickName = 'Kip'序列化時,將屬性名稱轉換為指定的 KeyCase 命名規則。套用於類層級時,轉換所有序列化 key;套用於屬性層級時,僅覆蓋該屬性的類層級設定。
toArray() / toJson() 的引數決定轉換行為:
false(預設):不套用任何 key 轉換,屬性名稱原樣輸出true:套用#[OutputKeyTo]定義的轉換,僅作用於當前層級,不向下滲透至巢狀物件(巢狀物件依各自的#[OutputKeyTo]宣告獨立運作)KeyCase::*:忽略#[OutputKeyTo],以指定的KeyCase強制覆蓋所有 key
use ReallifeKip\ImmutableBase\Attributes\OutputKeyTo;
use ReallifeKip\ImmutableBase\Enums\KeyCase;
// 類層級:將 nickName 序列化為 nick_name
#[OutputKeyTo(KeyCase::Snake)]
readonly class UserDTO extends DataTransferObject
{
public string $nickName;
}
UserDTO::fromArray(['nickName' => 'Kip'])->toArray(true); // ['nick_name' => 'Kip']可用的 KeyCase 值:
| 命名規則 | 範例 |
|---|---|
KeyCase::Snake |
nick_name |
KeyCase::PascalSnake |
Nick_Name |
KeyCase::Macro |
NICK_NAME |
KeyCase::Camel |
nickName |
KeyCase::Pascal |
NickName |
KeyCase::Kebab |
nick-name |
KeyCase::CamelKebab |
nick-Name |
KeyCase::Train |
Nick-Name |
全域嚴格模式,啟用時效果等同於對所有 ImmutableBase 子類套用 #[Strict]。
ImmutableBase::strict(true);啟用除錯記錄,輸入資料中多餘的 key 將會被記錄至 {$path}/ImmutableBaseDebugLog.log,包含時間戳、堆疊追蹤及輸入內容,傳入 null 停用紀錄。
ImmutableBase::debug(__DIR__); // 啟用除錯紀錄
ImmutableBase::debug(null); // 停用除錯紀錄載入預先透過 cacher 產生的屬性元資料快取,用以跳過執行期反射掃描、加速啟動速度,快取檔案存在時,會在首次載入 ImmutableBase 時自動載入快取,一般使用情境下不需要手動呼叫。
ImmutableBase::loadCache();掃描指定目錄中的所有 ImmutableBase 子類,產生序列化的元資料快取檔案 ib-cache.php,消除啟動時的反射開銷,需透過 ImmutableBase::loadCache() 載入快取。
# 預設:從根目錄開始掃描整個專案
vendor/bin/ib-cacher
# 指定:僅掃描特定目錄(例如 src 資料夾)並生成 ib-cache.php
vendor/bin/ib-cacher --scan-dir=src
# 清除:移除 ib-cache.php
vendor/bin/ib-cacher --clear為專案所有 ImmutableBase 子類物件產生文件,可產生 Mermaid 類別圖、Markdown 屬性表及 TypeScript 型別宣告。
vendor/bin/ib-writer所有例外皆繼承自 ImmutableBaseException,依據錯誤性質分為兩大類、三大主題,巢狀建構錯誤會在訊息中包含完整的屬性路徑,如:OrderDTO > $customer > $email > {錯誤訊息}。
類結構或 Attribute 配置有誤時拋出,屬於程式設計錯誤,通常在首次實例化進行反射掃描時觸發。
InvalidPropertyTypeException - 屬性宣告了不受支援的型別(如:iterable、object、非 ImmutableBase 子類或非 Enum 的類)。
InvalidVisibilityException - 屬性未宣告為 public。
InvalidArrayOfTargetException - #[ArrayOf] 指定的目標類不是 DTO、VO 或 SVO 的子類。
InvalidArrayOfUsageException - #[ArrayOf] 套用在非 array 型別的屬性上。
InvalidSpecException - #[Spec] 未提供引數或引數為空。
InvalidKeyCaseException - #[InputKeyTo] 或 #[OutputKeyTo] 接收到非 KeyCase enum 實例的值(例如傳入純字串而非 KeyCase::Camel)。
InvalidCompareTargetException - equals() 的比較對象與自身類不同,或陣列中包含無法比較的非 ImmutableBase 物件。
InvalidWithPathException - with() 的深層路徑指向純量屬性,無法向下展開。
DebugLogDirectoryInvalidException - ImmutableBase::debug() 指定的路徑不存在、不可寫或不是目錄。
建構(fromArray、fromJson)或變更(with)時,輸入資料不符合宣告的型別約束時拋出。
RequiredValueException - 非 nullable 屬性收到 null 或在輸入資料中缺失。
InvalidValueException - 值的型別與宣告的屬性型別不符。
InvalidEnumValueException - 值無法解析為目標 Enum 的任何 case,名稱查找及 tryFrom() 皆失敗。
InvalidJsonException - JSON 字串解碼失敗。
領域驗證失敗或結構約束違規時拋出。
ValidationChainException - VO、SVO 的 validate() 回傳 false。若類套用了 #[Spec],可透過 $exception->getSpec() 取得自定義訊息。
StrictViolationException - 嚴格模式下,輸入資料包含未宣告為屬性的 key。
InvalidArrayOfItemException - #[ArrayOf] 陣列中的某個元素無法解析為目標類的實例。
#[DataTransferObject], #[ValueObject], #[Entity]
#[DataTransferObject] 與 #[ValueObject] 已在 v4 移除。
請改用類別繼承:extends DataTransferObject / extends ValueObject。
#[Entity] 已在 v4 移除,不再支援 Entity 類型。
本段內容僅供 v3 使用者遷移參考。
- 所有子類屬性必須為 public;由於 ImmutableBase 為 readonly class,整條繼承鏈在 PHP 語言層級也必須是 readonly。
- 此體系子物件所有屬性型別禁止設為:
null、iterable、object、非 ImmutableBase 子類或非 Enum 的類,如:DateTime、Closure。 - Enum 屬性接受 case 名稱(
"HIGH")或 backed 值(3),解析後的屬性值始終為 Enum 實例。 - 支援
mixed型別,但值不會被進行驗證。
本套件使用 MIT License。
由 Kip 開發與維護,適用於所有 PHP 專案。
如果有任何建議或發現錯誤,歡迎開 PR 或提出 Issue。