Storage
Storage — общее хранилище значений в памяти отдельного PHP-процесса. Приложение обращается к нему через TCP-клиент localzet\Storage\Client; сервер localzet\Storage\Server выполняет команды над массивом данных.
Так можно обмениваться короткоживущим состоянием между worker-процессами. Данные исчезают при перезапуске сервера. Дискового журнала, TTL, репликации и восстановления после сбоя в реализации нет.
Требования и установка
Заголовок раздела «Требования и установка»Манифест требует PHP ^8.2 и Localzet Server ^4.1:
composer config repositories.localzet-storage vcs https://github.com/localzet/Storagecomposer require localzet/storage:dev-mainЗависимость от Server 4.x относится к этому пакету. Не считайте его совместимым с другой major-версией только из-за совпадения namespace.
Сервер в доверенной сети
Заголовок раздела «Сервер в доверенной сети»use localzet\Storage\Server;
require __DIR__ . '/vendor/autoload.php';
new Server('127.0.0.1', 8008);\localzet\Server::runAll();Сервер создаёт один worker с протоколом frame, именем StorageServer и отключённым reload. Один процесс нужен потому, что данные хранятся в обычном массиве объекта: несколько независимых процессов не разделят его автоматически.
Укажите адрес явно. Значение конструктора по умолчанию — 0.0.0.0:8008. Аутентификация, TLS и проверка права на отдельный ключ в обработчике отсутствуют. Сервер вызывает unserialize() без запрета классов; клиент также десериализует ответ. Обе стороны должны доверять друг другу и транспорту. Не открывайте этот сервис для пользовательских подключений или в интернет.
Чтение и запись
Заголовок раздела «Чтение и запись»use localzet\Storage\Client;
$storage = new Client('127.0.0.1:8008');
$storage->set('article:42', ['title' => 'Example']);$article = $storage->get('article:42');
$storage->delete('article:42');Клиент подключается лениво, при первой операции с ключом. Передавайте адрес без tcp://: префикс клиент добавляет сам. Соединение сохраняется и используется для следующих операций, пока остаётся открытым.
Все операции клиента блокирующие: запись и чтение выполняются через fwrite() и fread(). Если вызвать их из callback асинхронного Server, этот worker будет ждать ответ хранилища и не сможет в это время обслуживать другие события.
Ключи лучше задавать непустыми строками. Значения передаются через PHP serialize(): формат предназначен для PHP-клиентов с согласованными типами и версиями классов. Для межъязыкового API это не готовый JSON-протокол.
Установка только при отсутствии
Заголовок раздела «Установка только при отсутствии»$created = $storage->add('job:42', ['state' => 'queued']);add() возвращает true, если ключ ещё не определён, и false, если уже есть. Проверка использует isset(): сохранённое null считается отсутствием. Поэтому add('key', $value) может заменить ранее установленное null.
У операции нет TTL. Такой ключ сам не освободится после падения владельца, поэтому без дополнительного механизма срока действия его нельзя использовать как полноценную блокировку задания.
Счётчики
Заголовок раздела «Счётчики»$storage->add('downloads:42', 0);$count = $storage->increment('downloads:42');$count = $storage->increment('downloads:42', 5);increment() не создаёт отсутствующий счётчик: возвращает false. Существующее нечисловое значение сначала заменяется на 0, затем прибавляется шаг. Числовая строка считается числом по правилам PHP. Отрицательный шаг допустим как уменьшение значения.
Ответом является новое значение, которое может быть 0; проверяйте отказ через === false, а не через if (!$count).
Обновление с проверкой прежнего значения
Заголовок раздела «Обновление с проверкой прежнего значения»$old = $storage->get('job:42');$new = ['state' => 'running'];
if (!$storage->cas('job:42', $old, $new)) { // Another writer changed the value; read it again before retrying.}CAS сравнивает md5(serialize($old)) с сериализацией текущего значения на сервере. Если они совпали, сохраняет новое значение и возвращает true; иначе — false.
Для массивов важны порядок и типы элементов, для объектов — результат PHP-сериализации. Это сравнение состояния, а не проверка подлинности сообщения. MD5 здесь не защищает от недоверенного клиента.
Отсутствующий ключ сравнивается как null, поэтому cas($key, null, $value) способен создать значение. Несколько последовательных CAS над разными ключами не образуют транзакцию.
Несколько серверов
Заголовок раздела «Несколько серверов»$storage = new Client([ '127.0.0.1:8008', '127.0.0.1:8009',]);Клиент выбирает сервер по остатку CRC32 ключа от числа адресов. Один ключ направляется ровно на один сервер; копирования на остальные нет. При отказе выбранного сервера клиент не переключается на соседний.
Порядок адресов и их количество являются частью размещения данных. Их изменение перенаправит ключи без переноса прежних значений. Для стабильной работы все клиенты должны использовать одинаковый список в одинаковом порядке.
Таймауты и ошибки
Заголовок раздела «Таймауты и ошибки»$storage->timeout = 2;$storage->pingInterval = 25;timeout используется при установлении соединения и как socket timeout. Настройте его до первого запроса: уже открытое соединение не перенастраивается при изменении поля.
Ошибка соединения выбрасывает Exception с сообщением socket API; неполная запись — Exception('write fail'); пустой/неудачный ответ — Exception('read fail'). Это не отдельная типизированная иерархия ошибок протокола. Автоматического повторения команды нет. После потери ответа нельзя по одному исключению понять, успел ли сервер применить запись.
Если доступен localzet\Timer и клиент работает в CLI, при подключении регистрируется ping-timer. Он работает только при реально запущенном Timer/event loop. Ping не запрашивает подтверждение от сервера и не доказывает, что ключи сохранены. Если запись ping не удалась, соединение закрывается и timer удаляется.
Полный контракт операций и формата сообщений
Исходный код · AGPL-3.0