You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit abbeb76
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/blog/making-legal-states-impossible-in-place-typestates-in-pure-php.md
+39-54Lines changed: 39 additions & 54 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,7 +47,7 @@ These bugs exist because of a fundamental architectural blind spot we have polit
47
47
48
48
A closed `File` object will still cheerfully offer you a `read()` method. A `QueryBuilder` with zero fitlers will happily execute `delete()`. An idle database conection will smile and pretend it can `commit()`. The type system just sits there, sips its coffee, shrugs, and says: *"Sure, looks valid to me!"*
49
49
50
-
For nearly forty years, computer scientists have chased a holy grail designed to end this madness: **Typestate-Oriented Programming**, the radical, mind-bending proposition that an object's type should actually reflect its current state.
50
+
For nearly forty years, computer scientists have pursued a holy grail designed to end this madness: **Typestate-Oriented Programming**, the radical, mind-bending proposition that an object's type should actually reflect its current state.
51
51
52
52
Mainstream languages looked at this idea, nodded thoughtfully in academic conferences, and then did everything in their power to avoid implementing it properly. C# freezes the type in carbonite. Rust destroys the value and runs away with it. Static analyzers wave their hands and pray CI catches it.
53
53
@@ -153,23 +153,24 @@ Static analysis is great at catching typos before you commit. It is, by design,
153
153
154
154
We built a small typeatate machine that exercises three patterns that appear constantly in real production code:
1.Using generic defaults (`@template TState of State = Unauthenticated`) so we never even have to write `@var` comments on instantiation!
157
157
2. Passing the object through an alias (`$shared = $alice`), the classic shared-reference case that most PHP code relies on.
158
158
3. Sending it through an untyped framework boundary (`untypedLayer(object $s)`).
159
159
160
160
The same code was analyzed by **PHPStan at Level MAX** and then executed under **TypePHP's live runtime engine**.
161
161
162
-
### What PHPStan Reports: 5 False Positives
162
+
### What PHPStan Reports: 3 Errors (2 False Positives)
163
163
164
-

164
+

165
165
166
-
PHPStan surfaces a sea of red:
166
+
PHPStan halted with **3 errors**:
167
167
168
-
***Inside `login()` and `logout()`:** PHPStan refuses to accept the assignments to `$this->state`, treating the generic property as an immutable museum artifact. Even in massive codebases like Nextcloud core (`Response.php`), developers are forced to write `/** @psalm-suppress InvalidPropertyAssignmentValue */` just to shut the linter up!
169
-
***The Aliasing Blindspot:** After `$shared = $alice` and `$alice->login()`, PHPStan still insists `$shared` is unauthenticated. Flow analysis tracks variable *names*, not physical memory pointers, so the shared mutation is invisible to it.
170
-
***The Opaque Boundary:** Once `$bob` passes through `untypedLayer(object $s)`, the static model loses the transition and blindly rejects the later vault access.
168
+
***Line 55 (The Aliasing Blindspot):** PHPStan reported that `$shared` was unauthenticated, because static flow analysis tracks variable *names*, not physical heap pointers. It had no idea `$alice->login()` updated the shared instance in RAM.
169
+
***Line 71 (The Opaque Boundary):** Because `$bob` passed through `untypedLayer(object $s)`, PHPStan lost track of the transition and falsely claimed `$bob` could not access the vault.
170
+
***Line 78 (The Legitimate Catch):** PHPStan correctly caught `$s1` after it logged out.
171
+
172
+
Notice that out of 3 errors reported by PHPStan, **two were false positives on safe, legitimate code**, purely because static analysis cannot follow heap pointers.
171
173
172
-
None of these are bugs in PHPStan. They are the natural consequence of a tool that inspects text without ever executing it.
173
174
174
175
---
175
176
@@ -180,9 +181,9 @@ None of these are bugs in PHPStan. They are the natural consequence of a tool th
180
181
Now run the exact same code below inside TypePHP's live WebAssembly engine:
181
182
182
183
::: tip Try it yourself
183
-
1. Click **Run** below. All three accesses succeed cleanly with `Access granted!`, followed by line 92 halting with an uncaught `TypeError` because `$s1` logged out!
184
-
2. Click **Edit**, comment out line 90 (`$s1->logout();`), and run again. All four accesses now succeed with Exit 0!
185
-
3.[Open the exact same code in the PHPStan Playground](https://phpstan.org/r/54a4bc39-e250-4f4d-b649-d48f3af7727f) to see the 5 static false positives side by side.
184
+
1. Click **Run** below. All three accesses succeed cleanly with `Access granted!`, followed by line 78 halting with an uncaught `TypeError` because `$s1` logged out!
185
+
2. Click **Edit**, comment out line 76 (`$s1->logout();`), and run again. All four accesses now succeed with Exit 0!
186
+
3.[Open the exact same code in the PHPStan Playground](https://phpstan.org/try) to see the 3 static errors side by side.
186
187
:::
187
188
188
189
<TypePHPSnippettitle="Typestate Example: Static Analysis vs Runtime"default-view="source">
@@ -199,35 +200,19 @@ class Unauthenticated implements State {}
199
200
class Authenticated implements State {}
200
201
201
202
/**
202
-
* @template TState of State
203
+
* @template TState of State = Unauthenticated
203
204
*/
204
205
class Session
205
206
{
206
-
/**
207
-
* @var TState
208
-
*/
209
-
public object $state;
210
-
211
-
public function __construct()
212
-
{
213
-
$this->state = new Unauthenticated();
214
-
}
215
-
216
207
/**
217
208
* @phpstan-self-out self<Authenticated>
218
209
*/
219
-
public function login(): void
220
-
{
221
-
$this->state = new Authenticated();
222
-
}
210
+
public function login(): void {}
223
211
224
212
/**
225
213
* @phpstan-self-out self<Unauthenticated>
226
214
*/
227
-
public function logout(): void
228
-
{
229
-
$this->state = new Unauthenticated();
230
-
}
215
+
public function logout(): void {}
231
216
}
232
217
233
218
/**
@@ -241,25 +226,27 @@ function accessVault(Session $session): void
accessVault($s1); // Both agree: Throws TypeError on line 78!
283
270
```
284
271
285
272
</TypePHPSnippet>
286
273
287
274
TypePHP succeeds here for three concrete reasons:
288
275
289
-
1.**Speculative Property Rollbacks:**When `$this->state = new Authenticated()` runs inside `login()`, TypePHP’s `InlineChecker::trySelfOutTransition()`speculatively tests the impending `@self-out` transition and allows the assignment without requiring ugly suppress comments.
276
+
1.**Zero Boilerplate:**By declaring `@template TState of State = Unauthenticated`, `$s1 = new Session()`defaults cleanly to `Unauthenticated`without needing `@var` comments.
290
277
2.**Heap Pointer Aliasing:** It keys state by `spl_object_id`. When `$alice->login()` runs, the physical instance updates, so `$shared` reflects the change immediately.
291
-
3.**Observing the Actual Heap:** It observes what physically happened in memory, not what a static symbol graph guessed.
278
+
3.**Observing the Actual Heap:** It observes what physically happened in memory, not what a static symbol graph guessed. Both tools agree on line 78, but only TypePHP lets lines 55 and 71 run as intended.
292
279
293
280
---
294
281
@@ -361,20 +348,18 @@ Notice what this eliminates:
361
348
362
349
---
363
350
364
-
## Conclusion: PHP’s Accidental Advantage
365
-
366
-
For over twenty years, computer science purists have treated PHP’s lack of native C-level reified generics as proof that it is a toy language for building simple websites.
351
+
## Conclusion: Different Languages, Different Trade-Offs
367
352
368
-
That missing C-level rigidity turned out to be an unexpected gift:
353
+
It is easy to look at PHP's lack of native C-level reified generics and assume the language is behind compiled peers like C# or Rust. But language design is always an exercise in choosing the right trade-offs for a specific domain:
369
354
370
-
***C# and Swift**welded generic metadata into the heap header and locked it in carbonite forever.
371
-
***Rust** chose affine ownership and forced you to destroy your data just to change its clothes.
372
-
***TypePHP**took advantage of PHP's dynamic userland, decoupled generic metadata into `\WeakMap`, and simply mutated it in place, preserving the shared-reference model that PHP developers already take for granted.
355
+
***C# and Swift**prioritized raw throughput and cache-friendly memory layouts by embedding generic metadata directly into physical object headers, making in-place type changes an intentional impossibility for safety and performance reasons.
356
+
***Rust** chose affine ownership to guarantee thread safety and memory management without a garbage collector, which naturally models state machines by moving and consuming values rather than mutating shared references. It is a brilliant design for systems programming.
357
+
***TypePHP**takes advantage of PHP's dynamic, garbage-collected object model to decouple generic metadata into an external `\WeakMap`, evolving the type tag in place while keeping the shared-reference model that PHP developers rely on every day.
373
358
374
-
The result is actual**in-place Typestate-Oriented Programming**: state machines enforced by structural type contracts, without breaking reference identity, running in 100% pure PHP.
359
+
The result is practical**in-place Typestate-Oriented Programming**: state machines enforced by structural type contracts without breaking reference identity, all running in 100% pure PHP.
375
360
376
-
Sometimes the language everyone calls "dumb" is the only one flexible enough to do the smart thing.
361
+
Different languages make different compromises for their runtimes. In PHP's case, its dynamic userland turned out to be the exact architectural ingredient needed to make in-place typestates feel right at home.
377
362
378
363
***
379
364
380
-
*Have a state machine or workflow you would like to model? Check the dedicated [Mutable Generics & Typestates Guide](/docs/generics/mutable-generics-and-typestates) or try it live in the [Interactive WebAssembly Playground](/playground).*
365
+
*Have a state machine or workflow you would like to model? Check the dedicated [Mutable Generics & Typestates Guide](/docs/generics/mutable-generics-and-typestates) or try it live in the [Interactive WebAssembly Playground](/playground).*
0 commit comments