Skip to content

Trait Usage

The Immutable trait provides a mutate() method that creates modified copies of your objects while keeping the originals unchanged. This is the recommended approach when you control the class definition.

Add the trait and implement the Mutable contract:

<?php
use Cline\Immutable\Concerns\Immutable;
use Cline\Immutable\Contracts\Mutable;
final class UserData implements Mutable
{
use Immutable;
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly int $age,
) {}
}

The Mutable contract is optional but recommended - it documents that your class supports immutable mutation and enables IDE autocompletion.

$user = new UserData('John', 'john@example.com', 30);
$updated = $user->mutate(['age' => 31]);
// Original unchanged
$user->age; // 30
$updated->age; // 31
$updated = $user->mutate([
'name' => 'Jane',
'email' => 'jane@example.com',
]);
$updated = $user->mutate([
'name' => 'Jane',
'email' => 'jane@example.com',
'age' => 25,
]);

Pass an empty array to create an exact clone:

$clone = $user->mutate([]);
$clone !== $user; // true (different instance)
$clone->name === $user->name; // true (same values)

The trait handles complex property types including enums, DateTimeImmutable, and nested objects.

<?php
enum OperationStatus: string
{
case Pending = 'pending';
case Running = 'running';
case Completed = 'completed';
case Cancelled = 'cancelled';
}
final class OperationData implements Mutable
{
use Immutable;
public function __construct(
public readonly string $id,
public readonly OperationStatus $status,
public readonly int $progress,
public readonly ?DateTimeImmutable $completedAt,
) {}
}
$operation = new OperationData(
id: 'op-123',
status: OperationStatus::Running,
progress: 50,
completedAt: null,
);
$completed = $operation->mutate([
'status' => OperationStatus::Completed,
'progress' => 100,
'completedAt' => new DateTimeImmutable(),
]);
// Set nullable to value
$withDate = $operation->mutate(['completedAt' => new DateTimeImmutable()]);
// Set back to null
$withoutDate = $withDate->mutate(['completedAt' => null]);

The trait validates property types at runtime:

use Cline\Immutable\Exceptions\InvalidTypeException;
// Throws InvalidTypeException - age expects int
$user->mutate(['age' => 'thirty']);
// Message: Property "age" expects type "int", got "string".
// Throws InvalidTypeException - status expects enum
$operation->mutate(['status' => 'running']);
// Message: Property "status" expects type "OperationStatus", got "string".
  • Scalar types: int, float, string, bool
  • array
  • object
  • mixed
  • Class types (including enums)
  • Nullable types (?string, ?int, etc.)

Union and intersection types skip validation.

Non-existent properties throw an exception:

use Cline\Immutable\Exceptions\PropertyDoesNotExistException;
$user->mutate(['nonexistent' => 'value']);
// Throws: Property "nonexistent" does not exist on class "UserData".

Combine with PHP’s readonly keyword for maximum safety:

public function __construct(
public readonly string $name, // Cannot be modified after construction
public readonly string $email,
public readonly int $age,
) {}

Make classes final to prevent inheritance issues:

final class UserData implements Mutable
{
use Immutable;
// ...
}

Use Data Transfer Objects (DTOs) for complex state:

final class OrderData implements Mutable
{
use Immutable;
public function __construct(
public readonly string $id,
public readonly string $customerId,
public readonly OrderStatus $status,
public readonly Money $total,
public readonly DateTimeImmutable $createdAt,
public readonly ?DateTimeImmutable $shippedAt,
public readonly ?string $trackingNumber,
) {}
}
// Clean state transitions
$shipped = $order->mutate([
'status' => OrderStatus::Shipped,
'shippedAt' => new DateTimeImmutable(),
'trackingNumber' => 'TRACK123',
]);