PHPackages                             reallifekip/immutable-base - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. reallifekip/immutable-base

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

reallifekip/immutable-base
==========================

Strict immutable DTOs, VOs, and SVOs for PHP 8.4+ with construction-time type validation, deep path mutation, and automatic validation chaining.

v4.5.1(1mo ago)114352[1 PRs](https://github.com/ReallifeKip/ImmutableBase/pulls)2MITPHPPHP ^8.4CI passing

Since Jul 9Pushed 1mo agoCompare

[ Source](https://github.com/ReallifeKip/ImmutableBase)[ Packagist](https://packagist.org/packages/reallifekip/immutable-base)[ RSS](/packages/reallifekip-immutable-base/feed)WikiDiscussions main Synced yesterday

READMEChangelog (10)Dependencies (17)Versions (48)Used By (2)

ImmutableBase
=============

[](#immutablebase)

> 🌐 Available in other languages: [繁體中文](./README_TW.md)

[![License: MIT](https://camo.githubusercontent.com/a7e65aee57b11d28e4caff8b945729a66be0bb663f7f93bd24c5aa65699f148e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/a7e65aee57b11d28e4caff8b945729a66be0bb663f7f93bd24c5aa65699f148e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265)[![PHP Version Support](https://camo.githubusercontent.com/c5e4ad607d72942ced98e7b18f6d4298cea0f7665f022dd1ee4826e32045844c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/c5e4ad607d72942ced98e7b18f6d4298cea0f7665f022dd1ee4826e32045844c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d737175617265)[![Packagist Version](https://camo.githubusercontent.com/775789f52ac035cbf03f847bf62847b254383b4e84d151fb3e637f36ab96a4c5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/775789f52ac035cbf03f847bf62847b254383b4e84d151fb3e637f36ab96a4c5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d737175617265)

[![FOSSA Status](https://camo.githubusercontent.com/123fb2dc1ff6abb2633a9d24f1a5af7bac2f7ef3bd510042da75a9a4ffaaa48c/68747470733a2f2f6170702e666f7373612e636f6d2f6170692f70726f6a656374732f637573746f6d25324235373836352532466769746875622e636f6d2532465265616c6c6966654b6970253246496d6d757461626c65426173652e7376673f747970653d736d616c6c)](https://app.fossa.com/projects/custom%2B57865%2Fgithub.com%2FReallifeKip%2FImmutableBase?ref=badge_small)[![Coverage](https://camo.githubusercontent.com/585bb9761136d3d9afcfb5ea067f658b2c164839d466e02e4b930f709723e980/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f5265616c6c6966654b69702f496d6d757461626c65426173653f7374796c653d666c61742d737175617265266c6f676f3d636f6465636f7626636f6c6f723d323839653664)](https://camo.githubusercontent.com/585bb9761136d3d9afcfb5ea067f658b2c164839d466e02e4b930f709723e980/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f5265616c6c6966654b69702f496d6d757461626c65426173653f7374796c653d666c61742d737175617265266c6f676f3d636f6465636f7626636f6c6f723d323839653664)

[![Quality Gate Status](https://camo.githubusercontent.com/ab38cc0ee7f1e662b6e52726032f2d7ab52c0cacb16b619c1618ec0fc8197e39/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d616c6572745f737461747573)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Bugs](https://camo.githubusercontent.com/817992829264abdf922188fc4a8adbcef26f26a30605bac1de131eeaa8d73f48/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d62756773)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Code Smells](https://camo.githubusercontent.com/78a39b3cf65fbefc55f8c4326abb6dd12034548f4074a96ba526ad006725b2a4/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d636f64655f736d656c6c73)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Duplicated Lines (%)](https://camo.githubusercontent.com/caa7177f083d7f8a594d1f6ad75630cec7abf65dfccbd345bccfe1b74792a595/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d6475706c6963617465645f6c696e65735f64656e73697479)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Reliability Rating](https://camo.githubusercontent.com/450b62bcee3ecfc2e850b158b0f0aa61030ee8727465c4fefb6d6011c7f51c7b/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d72656c696162696c6974795f726174696e67)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Security Rating](https://camo.githubusercontent.com/5d32d3058c4fc2198831fa7aed984f70c5a3b42b804a13de2abe0addf76e986e/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d73656375726974795f726174696e67)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Technical Debt](https://camo.githubusercontent.com/8415cbeb737f39e3f5ff9cfb7eace9841ca3ee7b4310db9b12bf69f79393d5f4/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d7371616c655f696e646578)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Maintainability Rating](https://camo.githubusercontent.com/c5cb15ba892038d758f9f6a6913a73a593db0dfd0e2de3e21c6947fb2e669f1f/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d7371616c655f726174696e67)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)[![Vulnerabilities](https://camo.githubusercontent.com/f165e3dd98d412d0ee960840256b0ecdb2ed304b49f67766a536c50d6042a4fb/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d5265616c6c6966654b69705f496d6d757461626c6542617365266d65747269633d76756c6e65726162696c6974696573)](https://sonarcloud.io/summary/new_code?id=ReallifeKip_ImmutableBase)

[![CI](https://camo.githubusercontent.com/0a608c9895df53db5113a57b3550cf3ed34db2b3987e737f6afd6c9f55d0705a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f5265616c6c6966654b69702f496d6d757461626c65426173652f63692e796d6c3f7374796c653d666c61742d737175617265266c6f676f3d67697468756226636f6c6f723d323839653664266c6162656c3d4349)](https://camo.githubusercontent.com/0a608c9895df53db5113a57b3550cf3ed34db2b3987e737f6afd6c9f55d0705a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f5265616c6c6966654b69702f496d6d757461626c65426173652f63692e796d6c3f7374796c653d666c61742d737175617265266c6f676f3d67697468756226636f6c6f723d323839653664266c6162656c3d4349)[![Downloads](https://camo.githubusercontent.com/898a695c6a70c07b7154c7e8d3e3dee5d4661046894150f0773e5190cba40fd4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d73717561726526636f6c6f723d323839653664266c6162656c3d254630253946253933254136253230646f776e6c6f616473266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/898a695c6a70c07b7154c7e8d3e3dee5d4661046894150f0773e5190cba40fd4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7265616c6c6966656b69702f696d6d757461626c652d626173652e7376673f7374796c653d666c61742d73717561726526636f6c6f723d323839653664266c6162656c3d254630253946253933254136253230646f776e6c6f616473266c6f676f436f6c6f723d7768697465)

A PHP library for building **immutable data objects** with strict type validation, designed for **DTOs (Data Transfer Objects)**, **VOs (Value Objects)**, and **SVOs (Single Value Objects)**.

Focuses on **immutability**, **type safety**, and **deep structural operations** - including nested construction, dot, path mutation, and recursive equality comparison.

---

Why ImmutableBase?
------------------

[](#why-immutablebase)

### 🚀 Efficient Automatic Construction

[](#-efficient-automatic-construction)

```
// 🥳 ImmutableBase requires no boilerplate constructors. Pass an array or JSON to construct, with no ordering constraints on input keys.
readonly class Order extends DataTransferObject
{
    public string $date;
    public string $time;
}
Order::fromArray($data); // $data must be an array (use fromJson() for JSON strings)

// 🫤 The conventional approach requires writing constructors manually, cannot directly accept external array or JSON data for construction.
class Order extends DataTransferObject
{
    public function __construct(
        public readonly string $date,
        public readonly string $time
    ){}
}
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
```

### 🛡️ Declarative Default Values

[](#️-declarative-default-values)

```
// 🥳 ImmutableBase fills missing properties from defaultValues() or #[Defaults], with clear priority and null-awareness.
readonly class CreateUserDTO extends DataTransferObject
{
    public string $name;
    #[Defaults('member')]
    public string $role;

    public static function defaultValues(): array
    {
        return ['role' => 'admin']; // Takes precedence over #[Defaults]
    }
}
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'admin'

// 🫤 The conventional approach requires manual null-coalescing or constructor defaults, with no centralized declaration.
class CreateUserDTO {
    public function __construct(
        public readonly string $name,
        public readonly string $role = 'member', // Cannot be overridden per-class without rewriting constructors
    ){}
}
```

### 🔧 Flexible Deep Path Updates

[](#-flexible-deep-path-updates)

Update deeply nested properties by path - no Russian nesting dolls.

```
// 🥳 ImmutableBase is flexible and precise.
$order->with(['items.0.count' => 1]); // Target a specific array index and update count directly

// 🫤 The conventional approach is verbose and cannot preserve other elements in the original array.
$order->with([
    'items' => [
        [
            'count' => 1
        ]
    ]
])
```

### 🔎 Intuitive Error Tracing

[](#-intuitive-error-tracing)

```
// 🥳 ImmutableBase pinpoints the exact error location.
SomeException: Order > $profile > 0 > $count > {error message}

// 🫤 The conventional approach only provides vague or hard-to-trace messages.
SomeException: {error message}
```

### ⚡ Lightning-Fast Startup

[](#-lightning-fast-startup)

🥳 ImmutableBase can scan and generate a metadata cache file `ib-cache.php` via `vendor/bin/ib-cacher`, maximizing startup performance.

🫤 The conventional approach may lack any caching mechanism, paying the cost of reflection on every request.

### 🔗 Automatic and Controllable Validation Chain

[](#-automatic-and-controllable-validation-chain)

🥳 ImmutableBase's `ValueObject` and `SingleValueObject` support an optional `validate(): bool` method. During construction, the entire inheritance chain is automatically traversed top-down for validation. Apply `#[ValidateFromSelf]` to reverse the direction.

🫤 The conventional approach rarely offers an automatic validation chain - validation logic must be manually wired in constructors.

### 📃 Documentation as Code, Code as Documentation

[](#-documentation-as-code-code-as-documentation)

🥳 ImmutableBase can scan all subclasses in your project via `vendor/bin/ib-writer`, generating Mermaid class diagrams, Markdown property tables, and TypeScript declarations to keep documentation in sync with code.

🫤 The conventional approach cannot guarantee consistency between code and documentation.

### 🆓 Highly Compatible, Lightweight, Zero Dependencies

[](#-highly-compatible-lightweight-zero-dependencies)

🥳 ImmutableBase requires **no additional dependencies and is not tied to any framework** when used without documentation generation, caching, or testing.

🫤 The conventional approach, when coupled to a specific package or framework, is difficult to decouple quickly.

### 📦 Controllable Data Output

[](#-controllable-data-output)

```
// 🥳 ImmutableBase uses `#[KeepOnNull]` and `#[SkipOnNull]` to precisely control whether null properties appear in output - no manual filtering needed.
#[SkipOnNull]
readonly class User extends ValueObject
{
    #[KeepOnNull]
    public ?string $name;
    public ?int $age;
}
User::fromArray([])->toArray(); // ["name" => null]

// 🫤 The conventional approach typically requires manually filtering out null values.
readonly class User extends ValueObject
{
    public ?string $name;
    public ?int $age;
}

$user = new User();
$data = get_object_vars($user);
$data['name'] ??= null;
```

### ⭐ TypeScript-Like Type Narrowing

[](#-typescript-like-type-narrowing)

```
// 🥳 ImmutableBase constrains SingleValueObject to declare $value, but allows flexible type definitions. (Achieved via interface + hooked property with zero reflection overhead)
readonly class ValidAge extends SingleValueObject
{
    public int $value; // Semantically correct type matching the object's purpose
}

// 🫤 The conventional approach locks the type in the parent class with no way to customize it. Parents typically declare mixed or overly broad union types, making SVO design difficult.
class ValidAge extends SingleValueObject
{
    public string $value; // Type locked by parent - cannot be changed, semantically mismatched
}
```

---

Installation
------------

[](#installation)

```
composer require reallifekip/immutable-base
```

Requires PHP 8.4+.

---

Quick Example
-------------

[](#quick-example)

```
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],           // array
        '{"name": "Bob", "age": 19}',                     // JSON string
        User::fromArray(['name' => 'Carl', 'age' => 20]), // instance via fromArray
        User::fromJson('{"name": "Dave", "age": 21}'),    // instance via fromJson
    ],
    'userCount' => 4,
]);
```

---

Life Cycle
----------

[](#life-cycle)

 ```
%%{init: {'theme': 'base', 'themeVariables': {'background': '#0d1117', 'mainBkg': '#0d1117'}}}%%
flowchart TD
    classDef input fill:#0d1117,color:#79c0ff,stroke:#388bfd,stroke-width:2px
    classDef constructor fill:#0d1117,color:#ffa657,stroke:#d29922,stroke-width:2px
    classDef defaults fill:#0d1117,color:#d2a8ff,stroke:#8b949e,stroke-width:1px,stroke-dasharray:4
    classDef prepare fill:#0d1117,color:#d2a8ff,stroke:#8b949e,stroke-width:1px,stroke-dasharray:4
    classDef validate fill:#0d1117,color:#56d364,stroke:#3fb950,stroke-width:1px,stroke-dasharray:4
    classDef output fill:#0d1117,color:#ff7b72,stroke:#f85149,stroke-width:2px

    R([raw data]):::input

    R -->|array / json| F1
    R -->|scalar only| F2

    F1["fromArray() / fromJson()\n― DTO / VO ―"]:::constructor
    F2["from()\n― SVO ―"]:::constructor

    F1 & F2 --> DEF1

    DEF1["#[Defaults]"]:::defaults
    DEF1 --> DEF2
    DEF2["defaultValues()"]:::defaults

    DEF2 --> P["prepareInput()"]:::prepare

    P -->|DTO| I([instance]):::output
    P -->|VO / SVO| VAL["validate()"]:::defaults
    VAL --> I
```

      Loading > 🔗 Want a quick try? [JSON to ImmutableBase Converter](https://json-to-immutablebase-object-converter.reallife-kip.com) lets you paste JSON and generate IB classes instantly!

---

Testing
-------

[](#testing)

```
# Unit tests
vendor/bin/phpunit tests

# Benchmarks
vendor/bin/phpbench run
```

---

Object Types
------------

[](#object-types)

### DataTransferObject (DTO)

[](#datatransferobject-dto)

A pure data structure for transport and interchange. Even if a `validate(): bool` method is defined, it will not be invoked during construction.

```
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
use ReallifeKip\ImmutableBase\Objects\DataTransferObject;

readonly class SignUpUsersDTO extends DataTransferObject
{
    #[ArrayOf(User::class)]
    public array $users;
    public int $userCount;
}
```

### ValueObject (VO)

[](#valueobject-vo)

A semantically meaningful data structure that supports automatic validation during construction via a `validate(): bool` method.

```
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;
    }
}
```

### SingleValueObject (SVO)

[](#singlevalueobject-svo)

A semantically meaningful single value that supports automatic validation during construction via a `validate(): bool` method. The methods `validate()`, `from()`, `jsonSerialize()`, `__toString()`, and `__invoke()` all operate exclusively on the `$value` property.

```
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 (via __toString, string-casts $value)
echo $age();        // 18 (via __invoke)
echo $age->value;   // 18
```

---

API
---

[](#api)

### Construction - `fromArray()`, `fromJson()`

[](#construction---fromarray-fromjson)

Input keys that do not match declared properties are silently ignored (unless [strict mode](#strict---strict-mode) is enabled).

```
$user = User::fromArray(['name' => 'Kip', 'age' => 18]);
$user = User::fromJson('{"name": "Kip", "age": 18}');
```

### Construction - `from()` (SVO only)

[](#construction---from-svo-only)

```
$age = ValidAge::from(18);
```

### Serialization - `toArray()`, `toJson()`

[](#serialization---toarray-tojson)

```
$user->toArray();  // ['name' => 'ReallifeKip', 'age' => 18]
$user->toJson();   // {"name":"ReallifeKip","age":18}
```

### Mutation - `with()`

[](#mutation---with)

Updates specified properties and returns a **new instance**. The original object is never modified. Accepts an array, object, or JSON string.

```
$newUser = $user->with(['name' => 'Kip']);
$newUser = $user->with('{"name": "Kip"}');
$newUser = $user->with((object) ['name' => 'Kip']);
```

**Deep path syntax** - update nested properties via dot notation, bracket notation, or a custom separator:

```
// Dot notation
$newSignUp = $signUp->with(['users.0.name' => 'Kip']);
// Bracket notation
$newSignUp = $signUp->with(['users[0].name' => 'Kip']);
// Custom separator
$newSignUp = $signUp->with(['users/0/name' => 'Kip'], '/');
```

**SVO with()** - replaces the wrapped value directly:

```
$newAge = $age->with(20);
```

### Comparison - `equals()`

[](#comparison---equals)

Deep structural equality comparison. Works on all ImmutableBase subclasses. The comparison target must match in data, structure, and class. Nested ImmutableBase objects and arrays are compared recursively.

```
$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 - same data, different instances
$a->equals($c);  // false - age differs
```

For SVO subclasses, the wrapped `$value` is compared directly:

```
$age1 = ValidAge::from(18);
$age2 = ValidAge::from(18);
$age3 = ValidAge::from(20);

$age1->equals($age2);  // true
$age1->equals($age3);  // false
```

---

Default Values
--------------

[](#default-values)

Properties absent from input data can be populated with fallback values via two complementary mechanisms.

### `defaultValues()` — Dynamic Defaults

[](#defaultvalues--dynamic-defaults)

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.

```
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]` — Attribute Defaults

[](#defaults--attribute-defaults)

Apply `#[Defaults(value)]` to individual properties for inline constant-expression defaults. Constrained by PHP attribute syntax to scalars, arrays, and class constants.

```
use ReallifeKip\ImmutableBase\Attributes\Defaults;

readonly class CreateUserDTO extends DataTransferObject
{
    public string $name;
    #[Defaults('member')]
    public string $role;
    #[Defaults('en')]
    public string $locale;
}
```

### Resolution Priority

[](#resolution-priority)

When a property key is absent from the input data, defaults are resolved in this order:

1. `defaultValues()[$propertyName]`
2. `#[Defaults(value)]` attribute value
3. `null` (if nullable) or `RequiredValueException`

When both mechanisms define a value for the same property, `defaultValues()` takes precedence.

### Explicit `null` Is Not Absent

[](#explicit-null-is-not-absent)

When a key is present in the input with a `null` value, it is treated as an intentional assignment — default values are **not** applied.

```
readonly class Config extends DataTransferObject
{
    public ?string $theme;

    public static function defaultValues(): array
    {
        return ['theme' => 'dark'];
    }
}

Config::fromArray([]);                   // theme = 'dark' (key absent → default)
Config::fromArray(['theme' => null]);    // theme = null   (explicit null → respected)
Config::fromArray(['theme' => 'light']); // theme = 'light' (explicit value → used)
```

### Caching Behavior

[](#caching-behavior)

`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.

### SVO Restriction

[](#svo-restriction)

`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.

---

Preprocessing
-------------

[](#preprocessing)

Normalize, transform, or derive input values before hydration. Runs after all defaults are merged, before type resolution.

```
readonly class CreateUserDTO extends DataTransferObject
{
    public string $email;
    #[Defaults('member')]
    public string $role;

    protected static function prepareInput(array $data): array
    {
        return ['email' => strtolower(trim($data['email']))];
    }
}

CreateUserDTO::fromArray(['email' => '  BILL@EXAMPLE.COM  ']);
// email = 'bill@example.com', role = 'member'
```

### Key Behaviors

[](#key-behaviors)

**`$data` already contains defaults.** Both `defaultValues()` and `#[Defaults]` values are merged into `$data` before `prepareInput()` is called — you can read and transform them.

**Phantom-key injection is blocked.** Only keys already present in `$data` are written back. Returning a new key from `prepareInput()` is silently ignored.

```
protected static function prepareInput(array $data): array
{
    return [
        'email'   => strtolower($data['email']),
        'phantom' => 'injected',   // ignored — 'phantom' is not a property
    ];
}
```

**Inheritance chain stacks.** Each class in the hierarchy that declares its own `prepareInput()` runs in order from root to concrete. The parent's output feeds into the child's input.

```
readonly class ParentDTO extends DataTransferObject
{
    public string $name;

    protected static function prepareInput(array $data): array
    {
        return ['name' => trim($data['name'])];
    }
}

readonly class ChildDTO extends ParentDTO
{
    protected static function prepareInput(array $data): array
    {
        return ['name' => strtoupper($data['name'])];  // runs after parent trim
    }
}
```

**`with()` does not run `prepareInput()`.** `prepareInput()` is designed to sanitize untrusted external input (HTTP, CSV, API payloads). `with()` is a programmatic mutation by the developer, who is already the trusted caller — preparation is not applicable.

---

Attributes
----------

[](#attributes)

### `#[Defaults]` - Property Default Value

[](#defaults---property-default-value)

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.

```
use ReallifeKip\ImmutableBase\Attributes\Defaults;

readonly class CreateUserDTO extends DataTransferObject
{
    public string $name;
    #[Defaults('member')]
    public string $role;
}
CreateUserDTO::fromArray(['name' => 'Kip']); // role = 'member'
```

### `#[ArrayOf]` - Typed Array

[](#arrayof---typed-array)

Marks an array property as a typed collection of ImmutableBase instances or primitive scalar values. Each element is automatically validated or instantiated. The target must be a subclass of DTO, VO, or SVO, or a `Native` enum case for scalar arrays.

Pass multiple types for polymorphic arrays — each element is resolved in declaration order, first match wins.

**Primitive scalar arrays** can be declared using `Native` enum cases instead of a class name:

CasePHP type`Native::string``string``Native::int``int``Native::float``float``Native::bool``bool````
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;

readonly class SignUpUsersDTO extends DataTransferObject
{
    // ImmutableBase subclass
    #[ArrayOf(User::class)]
    public array $users;

    // Primitive scalar array
    #[ArrayOf(Native::string)]
    public array $tags;
    #[ArrayOf(Native::int)]
    public array $scores;

    // Polymorphic — first match wins
    #[ArrayOf(ShippingDTO::class, Native::string, PickupDTO::class, Native::int)]
    public array $deliveries;
}
```

### `#[Strict]` - Strict Mode

[](#strict---strict-mode)

Rejects input keys that do not correspond to declared properties.

```
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.
```

### `#[Lax]` - Lax Mode

[](#lax---lax-mode)

Exempts a class from strict mode enforcement, accepting input keys not declared as properties. Takes precedence over both `#[Strict]` and `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' => '...']); // constructs normally
```

### `#[SkipOnNull]` / `#[KeepOnNull]`

[](#skiponnull--keeponnull)

`#[SkipOnNull]` excludes null-valued properties from `toArray()` and `toJson()` output. Can be applied at class level (affects all properties) or property level (affects a single property). `#[KeepOnNull]` can only be applied at property level, overriding `#[SkipOnNull]` to retain the property in output even when null. Without `#[SkipOnNull]`, `toArray()` and `toJson()` include null-valued properties by default.

```
use ReallifeKip\ImmutableBase\Attributes\SkipOnNull;
use ReallifeKip\ImmutableBase\Attributes\KeepOnNull;

#[SkipOnNull]
readonly class UserDTO extends DataTransferObject
{
    #[KeepOnNull]
    public ?string $name;      // retained in output even when null
    public ValidAge|null $age; // excluded from output when null
}

UserDTO::fromArray([])->toArray();
// ['name' => null] (age excluded, name retained via KeepOnNull)
```

### `#[Spec]` - Validation Chain Info

[](#spec---validation-chain-info)

An optional message for VO and SVO classes. When `validate()` returns false, this message is included in the `ValidationChainException`. Consumers can retrieve it via `$exception->getSpec()`.

```
use ReallifeKip\ImmutableBase\Attributes\Spec;
use ReallifeKip\ImmutableBase\Exceptions\ValidationExceptions\ValidationChainException;

#[Spec('Age must be at least 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(); // Age must be at least 18
}
```

### `#[ValidateFromSelf]` - Validation Chain Reversal

[](#validatefromself---validation-chain-reversal)

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.

### `#[InputKeyTo]` - Input Key Case Conversion

[](#inputkeyto---input-key-case-conversion)

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.

```
use ReallifeKip\ImmutableBase\Attributes\InputKeyTo;
use ReallifeKip\ImmutableBase\Enums\KeyCase;

// Class-level: accepts snake_case input keys (nick_name → nickName)
#[InputKeyTo(KeyCase::Camel)]
readonly class UserDTO extends DataTransferObject
{
    public string $nickName;
}

UserDTO::fromArray(['nick_name' => 'Kip']); // nickName = 'Kip'
```

### `#[OutputKeyTo]` - Output Key Case Conversion

[](#outputkeyto---output-key-case-conversion)

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.

The argument passed to `toArray()` / `toJson()` controls the conversion behavior:

- `false` (default): no key conversion — property names are output as-is
- `true`: applies the `#[OutputKeyTo]`-defined conversion for the current level only — nested objects use their own `#[OutputKeyTo]` declarations independently and are not affected
- `KeyCase::*`: ignores `#[OutputKeyTo]` and forces the specified case globally

```
use ReallifeKip\ImmutableBase\Attributes\OutputKeyTo;
use ReallifeKip\ImmutableBase\Enums\KeyCase;

// Class-level: serializes nickName → nick_name
#[OutputKeyTo(KeyCase::Snake)]
readonly class UserDTO extends DataTransferObject
{
    public string $nickName;
}

UserDTO::fromArray(['nickName' => 'Kip'])->toArray(true); // ['nick_name' => 'Kip']
```

Available `KeyCase` values:

CaseExample`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`---

Configuration
-------------

[](#configuration)

### `ImmutableBase::strict(bool $on)`

[](#immutablebasestrictbool-on)

Global strict mode. When enabled, the effect is equivalent to applying `#[Strict]` to all ImmutableBase subclasses.

```
ImmutableBase::strict(true);
```

### `ImmutableBase::debug(?string $path)`

[](#immutablebasedebugstring-path)

Enables debug logging. Redundant keys in input data are logged to `{$path}/ImmutableBaseDebugLog.log`, including timestamps, stack traces, and input content. Pass `null` to disable.

```
ImmutableBase::debug(__DIR__); // enable debug logging
ImmutableBase::debug(null);    // disable debug logging
```

### `ImmutableBase::loadCache()`

[](#immutablebaseloadcache)

Loads pre-generated property metadata cache produced by `cacher`, bypassing runtime reflection scanning to speed up initialization. When the cache file exists, it is automatically loaded on the first autoload of ImmutableBase — manual invocation is not required under normal usage.

```
ImmutableBase::loadCache();
```

---

CLI Tools
---------

[](#cli-tools)

### `cacher` - Metadata Cache Generator

[](#cacher---metadata-cache-generator)

Scans all ImmutableBase subclasses in the specified directory and generates a serialized metadata cache file `ib-cache.php`, eliminating reflection overhead at startup. The cache is loaded via `ImmutableBase::loadCache()`.

```
# Default: Scans the entire project from the root directory
vendor/bin/ib-cacher

# Targeted: Scan a specific directory (e.g., src) and generates ib-cache.php
vendor/bin/ib-cacher --scan-dir=src

# Clear: Removes ib-cache.php
vendor/bin/ib-cacher --clear
```

### `writer` - Documentation Generator

[](#writer---documentation-generator)

Generates documentation for all ImmutableBase subclasses in the project. Supports Mermaid class diagrams, Markdown property tables, and TypeScript declarations.

```
vendor/bin/ib-writer
```

---

Error Handling
--------------

[](#error-handling)

All exceptions extend `ImmutableBaseException` and are categorized into two base types and three themes. Nested construction errors include the full property path in the message, e.g. `OrderDTO > $customer > $email > {error message}`.

### LogicException - Design Errors

[](#logicexception---design-errors)

#### DefinitionException - Definition Errors

[](#definitionexception---definition-errors)

Thrown when class structure or attribute configuration is incorrect. These are programming errors, typically triggered during reflection scanning on first instantiation.

`InvalidPropertyTypeException` - A property declares an unsupported type (e.g. `iterable`, `object`, non-ImmutableBase/non-Enum classes).

`InvalidVisibilityException` - A property is not declared as `public`.

`InvalidArrayOfTargetException` - The `#[ArrayOf]` target class is not a subclass of DTO, VO, or SVO.

`InvalidArrayOfUsageException` - `#[ArrayOf]` is applied to a property whose type is not `array`.

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

`InvalidKeyCaseException` - `#[InputKeyTo]` or `#[OutputKeyTo]` received a value that is not a `KeyCase` enum instance (e.g. a plain string instead of `KeyCase::Camel`).

`InvalidCompareTargetException` - The `equals()` comparison target is not the same class, or an array contains a non-ImmutableBase object that cannot be compared.

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

`DebugLogDirectoryInvalidException` - The path specified in `ImmutableBase::debug()` does not exist, is not writable, or is not a directory.

### RuntimeException - Runtime Errors

[](#runtimeexception---runtime-errors)

#### InitializationException - Initialization Errors

[](#initializationexception---initialization-errors)

Thrown during construction (`fromArray`, `fromJson`) or mutation (`with`) when input data does not satisfy declared type constraints.

`RequiredValueException` - A non-nullable property received null or is missing from the input data.

`InvalidValueException` - The value's type does not match the declared property type.

`InvalidEnumValueException` - The value cannot be resolved to any case of the target Enum; both name lookup and `tryFrom()` failed.

`InvalidJsonException` - The JSON string cannot be parsed into an object: malformed JSON, or non-empty list-rooted JSON (e.g. `[1,2,3]`). The empty literal `[]` is permitted to preserve `toJson()` → `fromJson()` round-trips when `#[SkipOnNull]` empties an associative DTO.

#### ValidationException - Validation Errors

[](#validationexception---validation-errors)

Thrown on domain validation failure or structural constraint violation.

`ValidationChainException` - A VO or SVO's `validate()` returned false. If the class has a `#[Spec]` attribute, the custom message can be retrieved via `$exception->getSpec()`.

`StrictViolationException` - Under strict mode, input data contains keys not declared as properties.

`InvalidArrayOfItemException` - An element in an `#[ArrayOf]` array cannot be resolved as an instance of the target class.

---

Deprecated
----------

[](#deprecated)

### Attributes

[](#attributes-1)

`#[DataTransferObject]`, `#[ValueObject]`, `#[Entity]`

---

Migration from v3 to v4
-----------------------

[](#migration-from-v3-to-v4)

\#\[DataTransferObject\] and #\[ValueObject\] are removed in v4.

Use class inheritance instead: extends DataTransferObject / extends ValueObject.

\#\[Entity\] is removed in v4, and Entity is no longer supported.

This section is provided for v3 migration reference only.

---

Notes
-----

[](#notes)

1. All subclass properties must be public. Since ImmutableBase is declared as a readonly class, the entire inheritance chain must also be readonly at the PHP language level.
2. Forbidden property types: `null`, `iterable`, `object`, non-ImmutableBase/non-Enum classes such as `DateTime`, `Closure`.
3. Enum properties accept case names (`"HIGH"`) or backed values (`3`). The resolved property value is always an Enum instance.
4. `mixed` type is supported, but values will not be validated.

---

License
-------

[](#license)

This package is released under the [MIT License](https://opensource.org/license/mit).

---

Maintainer
----------

[](#maintainer)

Developed and maintained by [Kip](mailto:bill402099@gmail.com). Suitable for all PHP projects.

---

Feedback and contributions are welcome - please open an Issue or submit a PR.

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance93

Actively maintained with recent releases

Popularity23

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 98.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Total

45

Last Release

35d ago

Major Versions

v1.1.0 → v2.0.02025-07-20

v2.4.5 → v3.0.02025-10-13

v3.1.3 → v4.0.0-rc.12026-02-26

PHP version history (3 changes)v1.0.0PHP ^8.0

v3.0.0PHP ^8.1

v4.0.0-rc.1PHP ^8.4

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/91828793?v=4)[Kip](/maintainers/ReallifeKip)[@ReallifeKip](https://github.com/ReallifeKip)

---

Top Contributors

[![ReallifeKip](https://avatars.githubusercontent.com/u/91828793?v=4)](https://github.com/ReallifeKip "ReallifeKip (63 commits)")[![Zhang-mason](https://avatars.githubusercontent.com/u/207661830?v=4)](https://github.com/Zhang-mason "Zhang-mason (1 commits)")

---

Tags

data-transfer-objectddddomain-driven-designdtofromarrayfromjsonimmutablephpphp84readonlyreflectionserializationsingle-value-objectsvotype-safetyvalidationvalue-objectvalueobjectvalueobjectsvophpvalidationreflectionValue Objectvoserializationdata-transfer-objectarray conversionDomain Driven Designddddtoimmutablephp84type-safetyreadonlytoArraysvotoJsonsingle-value-object

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/reallifekip-immutable-base/health.svg)

```
[![Health](https://phpackages.com/badges/reallifekip-immutable-base/health.svg)](https://phpackages.com/packages/reallifekip-immutable-base)
```

###  Alternatives

[fab2s/dt0

Immutable DTOs with bidirectional casting. No framework required. 8x faster than the alternative.

102.3k1](/packages/fab2s-dt0)[wendelladriel/laravel-validated-dto

Data Transfer Objects with validation for Laravel applications

762649.9k18](/packages/wendelladriel-laravel-validated-dto)[yorcreative/laravel-argonaut-dto

Argonaut is a lightweight Data Transfer Object (DTO) package for Laravel that supports nested casting, recursive serialization, and validation out of the box. Ideal for service layers, APIs, and clean architecture workflows.

1063.4k2](/packages/yorcreative-laravel-argonaut-dto)[friendsofhyperf/validated-dto

The Data Transfer Objects with validation for Hyperf.

1514.8k](/packages/friendsofhyperf-validated-dto)[event4u/data-helpers

Framework-agnostic PHP library for data mapping, DTOs and utilities. Includes DataMapper, SimpleDto/LiteDto, DataAccessor/Mutator/Filter and helper classes (MathHelper, EnvHelper, etc.). Works with Laravel, Symfony/Doctrine or standalone PHP.

1431.1k](/packages/event4u-data-helpers)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
