WeakMap, and the cache that does not leak

A memoisation cache keyed on object identity keeps every object it has ever seen alive, which in a long-running process is a leak with no upper bound.

final class Prices
{
    private WeakMap $cache;

    public function __construct()
    {
        $this->cache = new WeakMap();
    }

    public function for(Product $p): Money
    {
        return $this->cache[$p] ??= $this->calculate($p);
    }
}

// when $p is garbage collected, the entry goes with it.
// SplObjectStorage would have kept both forever.

The distinction from SplObjectStorage is that the key is a weak reference, so the map does not count as a reason to keep the object alive. That only matters in a process that outlives a request — a worker, a long import, an application server keeping the framework in memory — and in a normal web request the difference is invisible because everything is freed anyway. Keys must be objects; there is no scalar-keyed equivalent.