PHP Has WeakMap Too - and It Fixes Per-Object Caching
A pattern that looks innocent and isn't:
class ReportRenderer
{
private static array $cache = [];
public function render(Order $order): string
{
return self::$cache[spl_object_id($order)] ??= $this->expensiveRender($order);
}
}
Two bugs share that line. The leak: the static array holds rendered strings for every order ever seen, and in a long-running worker (queue consumers, Swoole/FrankenPHP, test suites) it only grows. The corruption: spl_object_id values are recycled after objects are destroyed - a new Order can receive a dead Order's id and inherit its cached render. Rare, maddening, real.
WeakMap: keyed by the object itself, weakly
class ReportRenderer
{
private WeakMap $cache;
public function __construct()
{
$this->cache = new WeakMap();
}
public function render(Order $order): string
{
return $this->cache[$order] ??= $this->expensiveRender($order);
}
}
Objects are the keys directly - no id extraction, no recycling hazard. And the references are weak: when the last real reference to an Order drops, the entry evaporates with it. The cache's memory profile tracks live objects by construction. No eviction policy to write, no TTLs, no leak.
Where it earns its keep
- Derived-data memoization: parsed metadata per entity, compiled validators per form definition, reflection results per class instance - anything expensive and object-tied.
- Decorating objects you don't own: attach bookkeeping to third-party or ORM objects without touching their classes - the same job JavaScript's WeakMap does for DOM nodes, same semantics, same reasoning.
- Long-running PHP specifically: in classic die-after-request PHP, leaks are capped by the request lifetime and static caches mostly get away with it. Workers changed that math - WeakMap is part of the long-running-PHP toolkit alongside resetting statics and watching memory_get_usage.
Semantics worth knowing
$map = new WeakMap();
$map[$order] = $rendered; // ArrayAccess API
count($map); // countable
foreach ($map as $obj => $val) {} // iterable - unlike JS WeakMap!
PHP's version is more observable than JavaScript's: it's countable and iterable (the GC story differs enough to allow it). Keys must be objects - scalars need a normal array, and if you find yourself wanting weak scalar keys, what you actually want is a real cache with eviction (APCu, Redis) because scalars have no lifetime to track.
One honest boundary: entries die with their keys, so never store the only reference to something inside a WeakMap value chain that loops back to the key - that's a self-releasing cache. For "data about objects whose lifecycle someone else owns", though, it's the correct structure, and almost nobody in PHP-land knows it shipped.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.