Слабые карты (Weak Maps) — это новая возможность, добавленная в PHP 8.0.

 WeakMap позволяет хранить и связывать  произвольные значения с объектами-ключами, но  не мешает сборщику мусора удалять объект, если он выходит из области видимости во всех остальных местах.

Weak Maps в PHP 8.0 отличаются от класса  WeakMap  из PECL-расширения  weakref . Подробнее читайте в статье Практические сценарии использования PHP WeakMap: PECL-расширение.

Класс  WeakMap  похож на  SplObjectStorage , так как и  WeakMap , и  splObjectStorage  используют  objects  в качестве ключа и позволяют хранить произвольные значения. Однако  WeakMap  не препятствует сборке мусора для объекта.

$map = new splObjectStorage();

$object = new stdClass();
$map[$object]  = 'Foo';

var_dump(count($map)); // int(1)

unset($object);

var_dump(count($map)); // int(1) 

В приведенном выше фрагменте кода  splObjectStorage  используется для хранения дополнительных данных об объектах. Несмотря на вызов  unset($object) , сборщик мусора PHP не удаляет его из памяти из-за ссылки внутри  $map   splObjectStorage .

При использовании  WeakMap  ссылки являются слабыми, и ссылка внутри  WeakMap  не мешает сборщику мусора удалить объект, если на него больше нет других ссылок.

- $map = new splObjectStorage();
+ $map = new WeakMap();

$object = new stdClass();
$map[$object]  = 'Foo';

var_dump(count($map)); // int(1)

unset($object);

var_dump(count($map)); // int(0) 

Более простой пример:

$map = new splObjectStorage();
$map[new stdClass()] = 'Foo';
count($map); // 1

$map = new WeakMap();
$map[new stdClass()] = 'Foo';
count($map); // 0 

 new stdClass  используется в качестве ключа и сразу же выходит из области видимости.  WeakMap  позволяет удалить объект из памяти, в то время как  splObjectStorage  — нет.

Слабые карты помогают связывать временные данные с объектами без изменения состояния самого объекта. Если объект выходит из области видимости, связанные с ним данные также удаляются.

Полный пример

class CommentRepository {
    private WeakMap $comments_cache;
    public function getCommentsByPost(Post $post): ?array {
        if (!isset($this->comments_cache[$post])) {
            $this->comments_cache[$post] = $this->loadComments($post);
        }
        return $this->comments_cache[$post]
    }
} 

В примере выше класс  CommentRepository  использует класс  WeakMap  для временного хранения комментариев в памяти в виде типизированного свойства  $comments_cache . При вызове метода  getCommentsByPost  с объектом  Post  сначала проверяется и возвращается значение из  $comments_cache , прежде чем загружать их из базы данных с помощью приватного метода  loadComments .

Если бы класс  CommentRepository  хранил комментарии в массиве (индексированном по ID поста) или в  SplObjectStorage , этот кэш не был бы очищен, даже если объект  Post  выйдет из области видимости или будет явно удален с помощью  unset($post) .

Благодаря  WeakMap  класс  CommentRepository  теперь может кэшировать данные (массив комментариев), не «удерживая» объекты  Post , но при этом связывая комментарии с объектами  Post . Состояние объекта  Post  не меняется (например, путем установки свойства  $post->comments ), а жизненный цикл комментариев управляется автоматически вместе со сборкой мусора самого объекта  Post .

Синопсис класса WeakMap

final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traversable {
    public function offsetGet(object $object);
    public function offsetSet(object $object, mixed $value): void;
    public function offsetExists(object $object): bool;
    public function offsetUnset(object $object): void;
    public function count(): int;
} 

Ключами WeakMap должны быть объекты

Слабые карты  принимают в качестве ключей только объекты, а связанные с ними значения  могут быть произвольными.

Попытка использовать любой другой тип данных в качестве ключа приведет к ошибке типа (TypeError).

$map = new WeakMap();
$map['Foo'] = 'Bar';

// Fatal error: Uncaught TypeError: WeakMap key must be an object in ...:... 

Добавление элементов в  WeakMap  через квадратные скобки без указания ключа также запрещено.

$map = new WeakMap();
$map[] = 'Baz';

// Fatal error: Uncaught Error: Cannot append to WeakMap in ...:... 

 Error  на несуществующих ключах

Если запрашиваемый объект отсутствует в объекте  weakmap , выбрасывается исключение  \Error .

$map = new WeakMap();
$map[new stdClass()];

// Fatal error: Uncaught Error: Object stdClass#2 not contained in WeakMap in ...:... 

Этого можно избежать, проверив существование индекса в карте.

$map = new WeakMap();
isset($map[new stdClass()]); // false 

WeakMap не поддерживает динамические свойства

$map = new WeakMap();
$map->foo = 'Bar';

// Fatal error: Uncaught Error: Cannot create dynamic property WeakMap::$foo in ...:... 

WeakMap не поддерживает сериализацию и десериализацию

Слабые карты нельзя сериализовать ( serialize ) или десериализовать ( unserialize ). Поскольку класс  WeakMap  объявлен как  final , изменить это поведение также невозможно.

$map = new WeakMap();
serialize($map);

// Fatal error: Uncaught Exception: Serialization of 'WeakMap' is not allowed in ...:... $serialized_str = 'C:7:"WeakMap":0:{}';
unserialize($serialized_str);

// Fatal error: Uncaught Exception: Unserialization of 'WeakMap' is not allowed in ...:... 

Итерация по WeakMap

Класс  WeakMap  реализует интерфейс  Traversable , поэтому его можно обходить с помощью конструкции  foreach .

Кроме того,  WeakMap  реализует  IteratorAggregate , который предоставляет метод  WeakMap::getIterator .

$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

foreach ($map as $key => $value) {
    var_dump($key); // var_dump($obj1)
    var_dump($value); // var_dump('Object 1');
} 

Сам итератор можно получить с помощью метода  getIterator , возвращаемое значение представляет собой  iterable .

$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

$iterator = $map->getIterator();

foreach ($iterator as $key => $value) {
    var_dump($key); // var_dump($obj1)
    var_dump($value); // var_dump('Object 1');
} 

Практические сценарии использования

Слабые карты помогают организовать временное хранение объектов без влияния на использование памяти самим этим временным хранилищем.

При использовании в качестве кэша ключи-объекты могут применяться для хранения примитивного состояния объекта, сокращая количество последующих вызовов ресурсоемкой функции, которая должна обрабатывать или дополнять объект.

Другим сценарием использования является хранение вспомогательных данных для коллекции объектов без изменения самих объектов.

Другие примеры использования Weak Maps в PHP 8.0 с примерами кода см. в статье Практические сценарии использования PHP WeakMap.

Влияние на обратную совместимость

Weak Maps — это новая функция в PHP 8.0, и если в глобальном пространстве имен нет класса с именем  WeakMap , при обновлении существующей кодовой базы до PHP 8.0 не должно возникнуть никаких проблем.

Поскольку  WeakMap  является встроенным классом, эту функциональность невозможно перенести на предыдущие версии PHP так, чтобы она работала с нативным сборщиком мусора. Существует пользовательская реализация (полифил), но она приводит к значительному снижению производительности.

PECL-расширение под названием  weakref предоставляло аналогичную функциональность, но оно больше не поддерживается.