skip to content

In a long-running PHP worker, when would you attach per-object data with WeakMap instead of SplObjectStorage, and why?

level: seniorimportance: should knowfreq 22%

answer

  1. who keeps the key object alive
  2. WeakMap since PHP 8.0, final
  3. missing key: Error versus UnexpectedValueException
  4. attach/detach/contains deprecated in 8.5
  5. spl_object_id() values get reused

basics

~20 s

SplObjectStorage holds strong references, so every object used as a key stays alive as long as the storage does, and a long-running worker leaks. WeakMap (PHP 8.0) holds keys weakly: an entry disappears when its object is destroyed elsewhere.

solid answer

~50 s

Both map objects to data, but they differ in who owns the key's lifetime. `SplObjectStorage` holds a **strong** reference to each object, so an object attached for caching lives until you remove it or drop the storage; in a queue consumer or daemon that runs for hours, that is steady memory growth. `WeakMap`, added in PHP 8.0, holds keys **weakly**: when the last other reference to an object goes, its entry is removed and `count()` drops. So for per-object caches or metadata about objects I do not own, I use `WeakMap`. I keep `SplObjectStorage` when the collection should own its members: a set of subscribers, set operations with `addAll()` and `removeAllExcept()`, custom identity through `getHash()`, or serialization, which `WeakMap` refuses. In PHP 8.5 I use array syntax on `SplObjectStorage`, because `attach()`, `detach()` and `contains()` are deprecated.

code

php · 24 lines
php
<?php
declare(strict_types=1);

final class PermissionCache
{
    private WeakMap $byUser;

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

    /** @return list<string> */
    public function permissionsFor(object $user, callable $compute): array
    {
        return $this->byUser[$user] ??= $compute($user);
    }

    public function size(): int { return count($this->byUser); }
}

$cache = new PermissionCache();
$user = new stdClass();
$cache->permissionsFor($user, fn () => ['read']);
echo $cache->size(), "\n"; // 1
unset($user);
echo $cache->size(), "\n"; // 0: the entry left with the object

go deeper

for a junior

Know that both classes map objects to data, and that WeakMap removes an entry when its key object is destroyed.

for a middle

Explain strong versus weak keys in terms of reference counting, and the API differences: missing-key errors, foreach shape, serialization and the 8.5 array-syntax replacements.

for a senior

Diagnose memory growth in a worker traced to an object-keyed cache, pick WeakMap or SplObjectStorage by ownership, and reject spl_object_id() arrays because ids are reused.

for a principal

Set a rule for long-running PHP processes: caches keyed by objects must be weak or bounded, and ownership of each collection must be explicit.

## Two ways to key data by object PHP arrays accept only `int` and `string` keys, so mapping an **object** to data needs a dedicated structure. The SPL and the engine provide two: - **`SplObjectStorage`** — an SPL class that is both an object set and an object-to-data map. - **`WeakMap`** — a `final` engine class, added in PHP 8.0, whose keys must be objects and are held weakly. Both use array syntax: `$map[$order] = $data`, `isset($map[$order])`, `unset($map[$order])`, `count($map)`, and `foreach`. ## The lifetime difference PHP frees an object when its reference count reaches zero. An `SplObjectStorage` entry **counts as a reference**, so the object cannot be freed while it is stored. A `WeakMap` entry **does not**; when every other reference to the key object is gone, the engine destroys the object and removes the entry. In a request that ends after 200 ms, the difference rarely matters: all memory is released at the end of the request. In a **long-running worker** — a queue consumer, a daemon, an event-loop server — it is the difference between a cache that shrinks as work finishes and one that keeps every order, entity or connection it ever saw. ## API differences that matter | | `SplObjectStorage` | `WeakMap` | |---|---|---| | Key held | strongly | weakly | | Read of a missing key | `UnexpectedValueException` ("Object not found") | `Error` ("Object … not contained in WeakMap") | | Non-object key | `TypeError` | `TypeError` ("WeakMap key must be an object") | | `foreach` yields | int position => object; data via `getInfo()` | object => value | | Set operations | `addAll()`, `removeAll()`, `removeAllExcept()` | none | | Custom identity | override `getHash()` | object identity only | | `serialize()` | supported | refused with an exception | | Subclassing | allowed | `final` | Both support `$map[$obj] ?? $default` and `isset()` without throwing. ## PHP 8.x changes to know 1. **8.0** added `WeakMap`. 2. **8.2** deprecated dynamic properties; the migration notes recommend a `WeakMap` for attaching data to objects you do not own instead of writing an undeclared property onto them. 3. **8.3** changed cycle handling: an entry whose value references its own key, possibly through other objects, may now be removed during cycle collection when the key is otherwise unreachable. Before 8.3 such entries were never removed automatically. 4. **8.5** deprecated `SplObjectStorage::attach()`, `detach()` and `contains()` in favour of `offsetSet()`, `offsetUnset()` and `offsetExists()`, which is what the array syntax calls. ## Why not an array keyed by spl_object_id() `spl_object_id()` returns an integer handle, so `$cache[spl_object_id($order)] = $data` looks like a cheap alternative. It has both problems at once: - The array holds no reference to the object, so nothing stops the object being destroyed while its entry lingers. - Handles are **reused**: once an object is freed, a new object can receive the same id and silently inherit the stale entry. `WeakMap` avoids both, because the entry is removed at the moment the key object is destroyed. ## Diagnosing the leak in a worker The symptom is a queue consumer whose resident memory climbs with every job and never comes back down, until it hits `memory_limit` or the supervisor restarts it. A practical sequence: 1. Log `memory_get_usage()` after each job; a steady staircase rather than a sawtooth points at something that retains per-job data. 2. Look for long-lived services that accept objects and store them: listeners, caches, identity maps, "seen" sets. 3. Check what they store into. An `SplObjectStorage`, or an array of objects, that is never pruned holds every job's entities forever. 4. Switch per-object caches to `WeakMap`, or clear the collection at the end of each job, and confirm the staircase flattens. ## Choosing - **Metadata or memoised results about objects owned elsewhere** (an entity's computed permissions, a request object's parsed body): `WeakMap`. - **A collection that owns its members** (registered listeners, a set of open connections you must close): `SplObjectStorage`, or an array if the members have natural string or int ids. - **Needs set algebra, serialization or a custom notion of sameness**: `SplObjectStorage`. The weak map's values are still held strongly; only the keys are weak. A value that holds its own key keeps the entry reachable through that value, which is why PHP 8.3's cycle change was needed.

  • In PHP 8.5, how do you add and test membership in SplObjectStorage without deprecation notices?
    Use array syntax: `$storage[$obj] = $info;` to add, `isset($storage[$obj])` to test and `unset($storage[$obj])` to remove. These call `offsetSet()`, `offsetExists()` and `offsetUnset()`, which replace `attach()`, `contains()` and `detach()`, deprecated in PHP 8.5.
  • Why does an SplObjectStorage cache hurt a long-running worker but not a normal web request?
    A web request ends quickly and PHP releases all of its memory at the end, so a strong-reference cache dies with it. A worker loops for hours in one process; every object attached to the storage stays alive until removed, so memory climbs with each job processed.
  • Why is `$cache[spl_object_id($obj)]` not a safe substitute for WeakMap?
    The array does not keep the object alive and is not told when it dies, so entries outlive their objects. Worse, PHP reuses object ids once an object is freed, so a new object can pick up a stale entry that belonged to a dead one.

saying these in an interview costs you the question

  • SplObjectStorage lets its objects be freed when nothing else uses them
  • WeakMap values are held weakly as well as its keys
  • An array keyed by spl_object_id() is a safe weak map
  • attach() and contains() are the preferred SplObjectStorage API in 8.5
  • WeakMap can be serialized to persist a cache