Skip to content

Commit b68d30c

Browse files
committed
Fix #81: Add article on immutability
1 parent 8116a1e commit b68d30c

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

guide/en/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Key concepts +
4242
* [Configuration](concept/configuration.md) +
4343
* [Aliases](concept/aliases.md) +
4444
* [Events](concept/events.md) +
45+
* [Immutability](concept/immutability.md) +
4546

4647
Handling requests +
4748
-----------------

guide/en/concept/immutability.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Immutability
2+
3+
Immutability means an object's state cannot change after it has been created.
4+
Instead of modifying an instance, you create a new instance with the desired changes.
5+
This approach is common for value objects such as Money, IDs, and DTOs. It helps to avoid accidental side effects:
6+
methods cannot silently change shared state, which makes code easier to reason about.
7+
8+
## Mutable pitfalls (what we avoid)
9+
10+
```php
11+
// A shared base query built once and reused:
12+
$base = Post::find()->where(['status' => Post::STATUS_PUBLISHED]);
13+
14+
// Somewhere deep in the code we only need one post:
15+
$one = $base->limit(1)->one(); // mutates the underlying builder (sticky limit!)
16+
17+
// Later we reuse the same $base expecting a full list:
18+
$list = $base->orderBy(['created_at' => SORT_DESC])->all();
19+
// Oops: still limited to 1 because the previous limit(1) modified $base.
20+
```
21+
22+
## Creating an immutable object in PHP
23+
24+
With modern PHP (8.2+), use `readonly` properties or a `readonly` class.
25+
That way, once constructed, properties cannot be reassigned at all.
26+
You still provide `with`- methods that return a new instance with changed data if you need to define an interface
27+
or need validation.
28+
29+
```php
30+
readonly final class Money
31+
{
32+
public function __construct(
33+
public int $amount,
34+
public string $currency,
35+
) {
36+
$this->validateAmount($amount);
37+
$this->validateCurrency($currency);
38+
}
39+
40+
private function validateAmount(string $amount)
41+
{
42+
if ($amount < 0) {
43+
throw new InvalidArgumentException('Amount must be positive.');
44+
}
45+
}
46+
47+
private function validateCurrency(string $currency)
48+
{
49+
if (!in_array($currency, ['USD', 'EUR'])) {
50+
throw new InvalidArgumentException('Invalid currency. Only USD and EUR are supported.');
51+
}
52+
}
53+
54+
public function withAmount(int $amount): self
55+
{
56+
$this->validateAmount($amount);
57+
58+
if ($amount === $this->amount) {
59+
return $this;
60+
}
61+
62+
$clone = clone $this;
63+
$clone->amount = $amount;
64+
return $clone;
65+
}
66+
67+
public function withCurrency(string $currency): self
68+
{
69+
$this->validateCurrency($currency);
70+
71+
if ($currency === $this->currency) {
72+
return $this;
73+
}
74+
75+
$clone = clone $this;
76+
$clone->currency = $currency;
77+
return $clone;
78+
}
79+
80+
public function add(self $money): self
81+
{
82+
if ($money->currency !== $this->currency) {
83+
throw new InvalidArgumentException('Currency mismatch. Cannot add money of different currency.');
84+
}
85+
return $this->withAmount($this->amount + $money->amount);
86+
}
87+
}
88+
89+
$price = new Money(1000, 'USD');
90+
$discounted = $price->withAmount(800);
91+
// $price is still 1000 USD, $discounted is 800 USD
92+
```
93+
94+
- `readonly` prevents property reassignment after construction; attempting to do so is a runtime error.
95+
- We mark the class `final` to prevent subclass mutations; alternatively, design for extension carefully.
96+
- Validate in the constructor and `with*` methods so every instance is always valid.
97+
98+
## Using clone (and why it is cheap)
99+
100+
PHP's clone performs a shallow copy of the object. For immutable value objects that contain only scalars
101+
or other immutable objects, shallow cloning is enough and fast. In modern PHP, cloning small value objects is
102+
inexpensive in both time and memory.
103+
104+
If your object holds mutable sub-objects that must also be copied, implement `__clone` to deep-clone them:
105+
106+
```php
107+
final class Order
108+
{
109+
public function __construct(
110+
private Money $total
111+
) {}
112+
113+
public function total(): Money
114+
{
115+
return $this->total;
116+
}
117+
118+
public function __clone(): void
119+
{
120+
// Money is immutable in our example, so deep clone is not required.
121+
// If it were mutable, you could do: $this->total = clone $this->total;
122+
}
123+
124+
public function withTotal(Money $total): self
125+
{
126+
$clone = clone $this;
127+
$clone->total = $total;
128+
return $clone;
129+
}
130+
}
131+
```
132+
133+
## Usage style
134+
135+
- Build a value object once and pass it around. If you need a change, use a `with*` method that returns a new instance.
136+
- Prefer scalar/immutable fields inside immutable objects; if a field can mutate, isolate it and deep-clone in `__clone`
137+
when needed.
138+
139+
Immutability aligns well with Yii's preference for predictable, side-effect-free code and makes services, caching,
140+
and configuration more robust.

0 commit comments

Comments
 (0)