🌐 Available in other languages: 繁體中文
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.
// 🥳 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 can be an array or JSON
// 🫤 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 specifiedUpdate 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
]
]
])// 🥳 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}🥳 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.
🥳 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.
🥳 ImmutableBase can scan all subclasses in your project via vendor/bin/ib-writer, generating Mermaid class diagrams and Markdown property tables to keep documentation in sync with code.
🫤 The conventional approach cannot guarantee consistency between code and documentation.
🥳 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.
// 🥳 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;// 🥳 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
}composer require reallifekip/immutable-baseRequires 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], // 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,
]);# Unit tests
vendor/bin/phpunit tests
# Benchmarks
vendor/bin/phpbench runA 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;
}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;
}
}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, only available when $value is a string)
echo $age(); // 18 (via __invoke)
echo $age->value; // 18Input keys that do not match declared properties are silently ignored (unless strict mode is enabled).
$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}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);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 differsFor 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); // falseMarks 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.
use ReallifeKip\ImmutableBase\Attributes\ArrayOf;
readonly class SignUpUsersDTO extends DataTransferObject
{
#[ArrayOf(User::class)]
public array $users;
public int $userCount;
}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.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] 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)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
}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.
Global strict mode. When enabled, the effect is equivalent to applying #[Strict] to all ImmutableBase subclasses.
ImmutableBase::strict(true);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 loggingLoads 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();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 --clearGenerates documentation for all ImmutableBase subclasses in the project. Supports Mermaid class diagrams and Markdown property tables.
vendor/bin/ib-writerAll 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}.
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.
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.
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 - JSON string decoding failed.
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.
#[DataTransferObject], #[ValueObject], #[Entity]
#[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.
- 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.
- Forbidden property types:
iterable,object, non-ImmutableBase/non-Enum classes such asDateTime,Closure. - Enum properties accept case names (
"HIGH") or backed values (3). The resolved property value is always an Enum instance. mixedtype is supported, but values will not be validated.
This package is released under the MIT License.
Developed and maintained by Kip. Suitable for all PHP projects.
Feedback and contributions are welcome - please open an Issue or submit a PR.