stougeiro / immutable
Immutable data structures for PHP arrays, providing strict separation between lists and objects, safe access semantics, key validation, and a clean, predictable API for defensive and expressive programming.
Fund package maintenance!
Requires
- php: >=8.1
Requires (Dev)
- pestphp/pest: ^5.0
- phpstan/phpstan: ^2.2
This package is auto-updated.
Last update: 2026-08-04 15:14:39 UTC
README
Immutable
Immutable is a rigorously designed data structure for PHP that enforces deep immutability across arrays, objects, and nested collections. It provides a predictable, safe, and expressive way to represent structured data without allowing accidental mutation, dynamic property creation, or silent type inconsistencies. Every value stored inside an Immutable instance is recursively normalized, ensuring that associative arrays become immutable objects, indexed arrays become immutable lists, and scalar values remain untouched. This strict normalization ensures that the structure you create is the structure you keep — permanently.
The core philosophy behind Immutable is to eliminate an entire class of bugs caused by unintended state changes. In traditional PHP arrays and objects, any part of the structure can be modified at any time, often far from where the data was originally created. Immutable prevents this by guaranteeing that once a structure is created, its shape and values cannot be altered — not directly, not indirectly, and not through nested references.
Immutable brings discipline and reliability to PHP’s flexible but mutation‑prone data structures. It enforces deep immutability, provides safe and expressive access patterns, and ensures that your data remains exactly as intended from the moment it is created. Whether used in small utilities or large-scale applications, Immutable helps developers write clearer, safer, and more predictable code.
✨ Features
-
Deep Immutability
Every nested element — objects, lists, and scalar values — is recursively wrapped and protected. Attempts to modify, unset, or overwrite any property or index result in explicit exceptions. Dynamic property creation, silently allowed in native PHP, is strictly forbidden. -
Strict Structural Semantics
Immutabledistinguishes between associative arrays (treated as objects) and indexed arrays (treated as lists). This distinction is preserved throughout the entire structure. Accessing an object as a list or a list as an object triggers an access exception, preventing ambiguous or incorrect usage. -
Safe Chained Access (isset‑safe)
Missing properties return null, allowing chained isset() evaluations to behave safely and predictably without producing fatal errors. -
Native PHP Errors for Invalid Direct Access
Immutable does not intercept invalid direct method calls on non-object values. When such operations occur, PHP’s native error system handles them, preserving natural language-level behavior. -
Explicit Error Signaling
Invalid operations — such as calling methods on non-object list items, accessing nonexistent properties, or attempting mutation — produce clear, intentional exceptions. This makes debugging significantly easier and prevents silent failures. -
Conversion Back to Native Arrays
ThetoArray()method reconstructs the entire immutable structure back into a native PHP array. This conversion is deep, ensuring that nested immutable objects and lists are fully unwrapped. The returned array is a copy, not a reference, guaranteeing that the original immutable structure remains untouched even if the consumer modifies the resulting array. -
No Silent Type Coercion
Immutable never converts lists into objects or objects into lists. If you attempt to access a list as an object or vice‑versa, an explicit exception is thrown.
📦 Installation
Install via Composer:
composer require stougeiro/immutable
🚀 Usage Example
Immutable Object Access
$data = Immutable::fromArray([ 'user' => [ 'name' => 'Sidney', 'email' => 'sidney@example.com', ], ]); echo $data->user->name; // "Sidney" echo $data->user->email; // "sidney@example.com"
This shows object-style access for associative arrays, preserving structure and preventing mutation.
Immutable List Access
$data = Immutable::fromArray([ 'roles' => ['admin', 'editor', 'viewer'], ]); echo $data->roles[0]; // "admin" echo $data->roles[1]; // "editor" echo $data->roles[2]; // "viewer"
Indexed arrays behave as immutable lists, allowing safe index access without converting items into objects.
Deep Conversion Back to Native Arrays
$data = Immutable::fromArray([ 'user' => [ 'name' => 'Sidney', 'roles' => ['admin', 'editor'], ], ]); $array = $data->user->toArray(); print_r($array); /* [ 'name' => 'Sidney', 'roles' => ['admin', 'editor'], ] */
toArray() deeply unwraps the immutable structure into a native PHP array, ensuring the original immutable data remains untouched.
Using Immutable as an API Response Wrapper (REST API)
$response = [ 'user' => [ 'id' => 10, 'name' => 'Sidney', 'roles' => ['admin', 'editor'], 'addresses' => [ [ 'type' => 'home', 'street' => 'Rua das Flores, 123', 'city' => 'São Paulo', 'zip' => '01000-000', ], [ 'type' => 'work', 'street' => 'Av. Central, 456', 'city' => 'Rio de Janeiro', 'zip' => '20000-000', ], ], ], ]; $data = Immutable::fromArray($response); // Safe access echo $data->user->name; // "Sidney" echo $data->user->addresses[1]->city; // "Rio de Janeiro" // Convert back to array for output return json_encode($data->toArray());
This ensures your API layer never mutates the response data accidentally.
Using Immutable Inside a DTO (Data Transfer Object)
DTOs are perfect for Immutable because they represent read‑only structured data.
class UserDTO { public function __construct( public Immutable $data ) {} } $userDto = new UserDTO( Immutable::fromArray([ 'user' => [ 'name' => 'Sidney', 'roles' => ['admin', 'editor'], 'addresses' => [ [ 'type' => 'home', 'street' => 'Rua das Flores, 123', 'city' => 'São Paulo', 'zip' => '01000-000', ], [ 'type' => 'work', 'street' => 'Av. Central, 456', 'city' => 'Rio de Janeiro', 'zip' => '20000-000', ], ], ], ]) ); // DTO usage echo $userDto->data->user->roles[0]; // "admin" echo $userDto->data->user->addresses[0]->zip; // "01000-000"
This pattern guarantees that service layers cannot mutate DTO data, preserving integrity across the application.
Using Immutable to Normalize External Service Data (Microservices / Integrations)
Imagine receiving data from a microservice or external provider. Immutable ensures the structure is safe and predictable.
function fetchUserFromService(): Immutable { $payload = externalService()->get('/user/10'); return Immutable::fromArray($payload); } $data = fetchUserFromService(); // Safe deep access $city = $data->user->addresses[0]->city; // "São Paulo" // Safe isset chain if (isset($data->user->addresses[1]->zip)) { // zip exists // "20000-000" } // Deep conversion for logging or debugging log(print_r($data->toArray(), true));
This pattern is extremely useful in microservices, queue consumers, event-driven systems, API gateways or domain boundaries. It prevents accidental mutation of external data.
🔌 Easy Integration
Immutable was designed to fit naturally into any PHP application architecture — from small scripts to full frameworks — without requiring adapters, service providers, or special configuration. Because it behaves like a read‑only data layer, it integrates seamlessly into DTOs, API responses, service layers, and domain logic.
To support predictable return types and stronger system expectations, Immutable provides an ImmutableInterface, allowing developers to type‑hint immutable structures throughout the application. This ensures consistent behavior and prevents accidental mutation at architectural boundaries.
Immutable also includes a dedicated factory method, Immutable::fromArray(), which normalizes any array into a fully immutable structure. For convenience, a global helper function immutable() is available, offering a concise and expressive way to create immutable data wherever needed.
🧠 Why Immutable?
PHP’s dynamic nature makes it easy to accidentally mutate data, especially when passing arrays through multiple layers of an application. A single unintended assignment can corrupt shared state, break assumptions, or introduce subtle bugs that are difficult to trace.
Immutable solves this by enforcing:
- predictability
- consistency
- safety
- clarity of intent
It is ideal for:
- configuration objects
- DTOs
- domain models
- API responses
- cached data
- value objects
- any scenario where data integrity matters
Design Goals
- Prevent accidental mutation at all levels
- Preserve the original structure exactly as provided
- Provide intuitive access to nested data
- Fail loudly and clearly on invalid operations
- Maintain strict separation between objects and lists
- Offer ergonomic chained access without sacrificing safety
- Avoid magic behaviors that hide errors or distort semantics
🤝 Contributions
Contributions are welcome. Feel free to open issues or submit pull requests.