<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · SENIOR

Коли потрібні WeakMap і WeakReference?

WeakReference (PHP 7.4+) і WeakMap (PHP 8.0+) тримають обʼєкт, не збільшуючи його refcount: як тільки обʼєкт знищується, посилання стає null, а запис у WeakMap зникає сам — це штатний спосіб робити привʼязані до обʼєктів кеші й реєстри в довгоживучих процесах.

Ви кешуєте розраховані права в масиві за spl_object_id — що з цим не так у воркері Octane?
Чим WeakMap відрізняється від SplObjectStorage?
Як привʼязати дані до обʼєкта, не подовживши йому життя?
Чому запис у WeakMap не зник, хоча я зробив unset() ключа?
WeakMap WeakReference памʼять PHP 8.0 PHP 7.4 воркери GC

Основа памʼяті в PHP — підрахунок посилань: кожен обʼєкт має refcount, і щойно він падає до нуля, обʼєкт знищується негайно, з викликом __destruct(). Будь-яка звичайна змінна, властивість чи елемент масиву, що вказує на обʼєкт, цей лічильник збільшує — тобто утримує обʼєкт живим. Слабке посилання — це виняток із правила: WeakReference::create($obj) (PHP 7.4+) запамʼятовує обʼєкт, не чіпаючи refcount. Поки на обʼєкт є хоч одне сильне посилання, $weak->get() повертає його; коли останнє зникає, обʼєкт знищується за звичайним сценарієм, а get() починає повертати null. До 7.4 це вміла лише PECL-розширення weakref, тож на старих проєктах ви цього API не побачите.

WeakMap (PHP 8.0+) — надбудова над тією ж ідеєю: мапа, де ключ — обʼєкт (тільки обʼєкт, скаляр дасть TypeError), значення — будь-що, і слабким є саме ключ. Клас реалізує ArrayAccess, Countable та IteratorAggregate, тому працює як звичайний масив: $map[$obj] = $data, isset($map[$obj]), count($map), foreach ($map as $obj => $data). Головна властивість — запис видаляється автоматично разом зі знищенням ключа, без жодного коду очищення з вашого боку. Це те, чого не дають альтернативи: SplObjectStorage теж мапить обʼєкти на дані, але тримає ключі сильно, а масив, індексований spl_object_id(), обʼєкт не тримає, зате накопичує мертві записи й ризикує колізією — ідентифікатор знищеного обʼєкта PHP може видати новому, і кеш віддасть чужі дані.

Практичний сенс зʼявляється там, де процес живе довго: Octane, RoadRunner, Swoole, queue:work у режимі демона, довгі CLI-команди, ReactPHP/Amp. У класичному FPM-запиті все одно все звільняється наприкінці, тому витік у пер-обʼєктному кеші там непомітний; у демоні той самий статичний масив росте від першого запиту до перезапуску воркера. Канонічний сценарій для WeakMap — метадані, привʼязані до обʼєкта і не потрібні довше за нього: обчислені права користувача, memoized-результат дорогого методу для конкретної сутності, стан гідратації або ознака «цей обʼєкт уже провалідовано» у межах одного job. У прикладі PermissionResolver кеш живе рівно стільки, скільки живе $user: count() сам падає до нуля, коли воркер відпускає сутність.

Слабкість односпрямована, і на цьому найчастіше спотикаються. WeakMap тримає значення сильно, тому будь-який шлях від значення назад до ключа перетворює слабкий запис на вічний: $map[$order] = ['owner' => $order] з останнього блоку коду означає ланцюжок WeakMap → масив → $order, і unset($order) уже нічого не змінює. Лікується це зберіганням даних без зворотного посилання або загорнутим WeakReference усередині значення. Друга межа — цикли: якщо обʼєкт-ключ входить у циклічний граф (батько тримає дитину, дитина тримає батька), refcount ніколи не досягне нуля сам, і запис зникне лише після проходу збирача циклів, тож у тестах перед перевіркою count() варто викликати gc_collect_cycles(). Третя — обидва класи не серіалізуються: serialize() на них кине виняток, тому в сесію, кеш чи payload черги вони не потрапляють.

Нарешті, варто чесно назвати, чим WeakMap не є. Це не кеш із витісненням: у нього немає ані TTL, ані ліміту розміру, ані LRU — єдиний критерій видалення це смерть ключа. Якщо обʼєкт живе весь процес (синглтон із контейнера, case enum, зареєстрований слухач), WeakMap не дасть нічого, крім зайвої непрямості, і звичайна властивість буде чеснішою. І це не заміна аналізу витоків: якщо памʼять воркера росте, спершу треба знайти, хто тримає посилання (php-memprof, підрахунок живих обʼєктів у контрольних точках, memory_get_usage(true) і gc_status() у метриках), і лише потім вирішувати, чи справді проблемне місце — це реєстр, якому слабких ключів вистачить.

final class PermissionResolver
{
    /** @var WeakMap<User, list<string>> ключ тримається слабко */
    private WeakMap $cache;

    public function __construct(private readonly PermissionStorage $storage)
    {
        $this->cache = new WeakMap;          // PHP 8.0+
    }

    /** @return list<string> */
    public function for(User $user): array
    {
        // ??= працює через ArrayAccess: offsetExists → offsetGet → offsetSet
        return $this->cache[$user] ??= $this->storage->load($user->id);
    }

    public function cachedCount(): int
    {
        return count($this->cache);          // WeakMap реалізує Countable
    }
}

$resolver = new PermissionResolver($storage);
$user = new User(42);
$resolver->for($user);
echo $resolver->cachedCount();               // 1

// WeakReference (PHP 7.4+) не інкрементує refcount; new заборонено
$weak = WeakReference::create($user);
unset($user);                                // зникло останнє сильне посилання

var_dump($weak->get());                      // NULL — обʼєкт уже знищено
echo $resolver->cachedCount();               // 0 — запис пішов разом із ключем

// Пастка: значення тримає ключ, слабкість працює лише в один бік
$map = new WeakMap;
$order = new Order;
$map[$order] = ['owner' => $order];          // WeakMap → значення → ключ
unset($order);
echo count($map);                            // 1 — памʼять не звільниться
Що слабке посилання не інкрементує refcount: обʼєкт звільняється в момент, коли зникає останнє *сильне* посилання, а слабке після цього просто перестає працювати.
Що WeakMap — це мапа «обʼєкт → будь-яке значення» зі слабким ключем: запис видаляється автоматично разом із ключем, тому `count($map)` зменшується сам, без вашого коду очищення.
Що SplObjectStorage і звичайний масив, індексований `spl_object_id()`, цю задачу не розвʼязують: перший тримає обʼєкти сильно, другий накопичує мертві записи, а ідентифікатори ще й перевикористовуються після знищення обʼєкта.
Що проблема стає видимою саме в довгих процесах — Octane, RoadRunner, Swoole, `queue:work` у режимі демона, ReactPHP/Amp, — бо між запитами процес не помирає і статичні реєстри ростуть вічно.
Що значення у WeakMap тримається сильно: якщо воно (прямо чи через ланцюжок) посилається на ключ, запис не звільниться ніколи — слабкість односпрямована.
Що WeakMap не скасовує збирач циклів: якщо ключ у циклічному графі, запис зникне лише після проходу gc_collect_cycles, а не миттєво.
Казати «WeakMap — це кеш, який сам чиститься за LRU/TTL»: він нічого не витісняє за розміром чи часом, єдиний критерій — смерть ключа.
Класти у WeakMap рядок або int як ключ: `$map['user-42'] = ...` дає TypeError, ключем може бути тільки обʼєкт.
Робити `new WeakReference($obj)` — пряма інстанціація заборонена, є лише `WeakReference::create($obj)`.
Зберігати у значенні WeakMap сам обʼєкт-ключ (`$map[$o] = ['owner' => $o]`) і дивуватись, що памʼять не звільняється: значення тримає ключ живим.
Використовувати WeakMap для обʼєктів, які й так живуть весь процес — синглтонів із контейнера, case-ів enum, зареєстрованих слухачів: слабкість там не дає нічого, крім накладних витрат.
Вважати `unset($obj)` гарантією звільнення: якщо на обʼєкт лишилось інше сильне посилання (у логері, у події, у замиканні зі звʼязаним `$this`), `WeakReference::get()` і далі поверне обʼєкт.
ПОРАДА

Формулюйте через refcount: «сильне посилання каже обʼєкту жити, слабке — лише питає, чи він ще живий». Далі одне речення практики: у довгому воркері метадані, привʼязані до обʼєкта, тримають у WeakMap, бо масив за `spl_object_id` там перетворюється на витік із перевикористаними ключами.

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Слабким є саме ключ: WeakMap не збільшує refcount обʼєкта-ключа, і запис видаляється в момент знищення обʼєкта. Значення тримається сильно — тому посилання зі значення на ключ робить запис вічним. Ані витіснення за розміром/памʼяттю, ані слабкості значень тут немає, а SplObjectStorage, навпаки, тримає ключі сильно.