Skip to content

Instantly share code, notes, and snippets.

@jfinstrom
Last active June 22, 2025 22:59
Show Gist options
  • Save jfinstrom/d223edc682562b7929a7784bf006b9cd to your computer and use it in GitHub Desktop.
Save jfinstrom/d223edc682562b7929a7784bf006b9cd to your computer and use it in GitHub Desktop.
Memory footprint? Pshhhhhh That is why god made more RAM!!!

Introduction

If people didn't want us to use more RAM they would quit adding more to their system.

— Some Chrome Developer, Probably

So we have had generators since PHP 5.5. But memory be damned we just build ungoddly arrays and figure

Example Usage

In your module's code, where $this->FreePBX is available require_once 'StreamingKvReader.php';

  1. Initialize the reader with the PDO object and your module's class name
$pdo = $this->FreePBX->Database;
$this->reader = new \FreePBX\StreamingKvReader($pdo, self::class);
  1. Stream all device mappings without high memory usage
echo "--- Streaming all device mappings for id 'some_device_group' ---\n";
$deviceMappings = $reader->getAllGenerator('some_device_group');

foreach ($deviceMappings as $mapping) {
	// Process one device at a time. Memory is flat.
	echo "Device Key: " . $mapping['key'] . " has MAC: " . $mapping['value']['mac_address'] . "\n";
}
  1. Get just the keys
echo "\n--- Streaming just the keys ---\n";
foreach ($reader->getAllKeysGenerator('some_device_group') as $key) {
	echo "Found key: $key\n";
}
  1. Get a single, specific value
echo "\n--- Getting a single value ---\n";
$specificDevice = $reader->getConfigValue('device123', 'some_device_group');
print_r($specificDevice);
<?php
namespace FreePBX;
use PDO;
use Generator;
/**
* A memory-efficient, read-only helper for the FreePBX key-value store.
*
* This class uses PHP generators to stream data directly from the database,
* avoiding loading large result sets into memory. It is intended as a
* high-performance alternative to the fetching methods in DB_Helper.
*/
class StreamingKvReader
{
private PDO $pdo;
private string $tableName;
/**
* @param PDO $pdo The database connection object.
* @param string $moduleClass The fully qualified class name of the module (e.g., MyModule\Main::class). This is used to derive the table name.
*/
public function __construct(PDO $pdo, string $moduleClass)
{
$this->pdo = $pdo;
$this->tableName = $this->deriveTableName($moduleClass);
}
/**
* Derives the kvstore table name from a module's class name.
*
* @param string $className The FQCN of the module.
* @return string The derived table name, e.g., 'kvstore_MyModule_Main'.
*/
private function deriveTableName(string $className): string
{
// This mirrors the logic from the original DB_Helper.
return "kvstore_" . str_replace('\\', '_', $className);
}
/**
* getConfigGenerator: Fetches a single value.
*
* Note: A generator is not beneficial here as we only expect one result.
* This method performs a direct fetch.
*
* @param string $key The key to retrieve.
* @param string $id The optional sub-group ID.
* @return mixed The unserialized value, or null if not found.
*/
public function getConfigValue(string $key, string $id = 'noid')
{
$sql = sprintf(
"SELECT `val`, `type` FROM `%s` WHERE `key` = :key AND `id` = :id",
$this->tableName
);
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':key' => $key, ':id' => $id]);
$res = $stmt->fetch(PDO::FETCH_ASSOC);
return $res ? $this->hydrateValue($res['val'], $res['type']) : null;
}
/**
* getAllGenerator: Streams all key-value pairs for a given ID.
*
* This is the primary generator method. It fetches all rows for an ID
* and yields them one by one, keeping memory usage minimal.
*
* @param string $id The sub-group ID.
* @return Generator Yields an associative array ['key' =>, 'value' =>] for each row.
*/
public function getAllGenerator(string $id = 'noid'): Generator
{
// A single query to get all data for the ID.
$sql = sprintf(
"SELECT `key`, `val`, `type` FROM `%s` WHERE `id` = :id ORDER BY `key`",
$this->tableName
);
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
// Fetch and yield one row at a time. The full result set never exists in PHP memory.
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield [
'key' => $row['key'],
'value' => $this->hydrateValue($row['val'], $row['type']),
];
}
}
/**
* getAllKeysGenerator: Streams all keys for a given ID.
*
* @param string $id The sub-group ID.
* @return Generator Yields a string key for each row.
*/
public function getAllKeysGenerator(string $id = 'noid'): Generator
{
$sql = sprintf(
"SELECT `key` FROM `%s` WHERE `id` = :id ORDER BY `key`",
$this->tableName
);
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
while ($key = $stmt->fetch(PDO::FETCH_COLUMN)) {
yield $key;
}
}
/**
* getAllIdsGenerator: Streams all distinct IDs in the table.
*
* @return Generator Yields a string ID for each distinct ID found.
*/
public function getAllIdsGenerator(): Generator
{
$sql = sprintf(
"SELECT DISTINCT(`id`) FROM `%s` WHERE `id` <> 'noid'",
$this->tableName
);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
while ($id = $stmt->fetch(PDO::FETCH_COLUMN)) {
yield $id;
}
}
/**
* Hydrates a raw database value based on its type hint.
*
* @param string $value The raw value from the database.
* @param ?string $type The type hint (e.g., 'json-arr', 'json-obj').
* @return mixed The processed value.
*/
private function hydrateValue(string $value, ?string $type)
{
// This mirrors the type-handling logic from the original getConfig.
// Note: The original 'blob' logic is complex and likely needs a separate getBlob call.
// For this streaming reader, we assume blobs are handled elsewhere or are not the primary use case.
return match ($type) {
'json-arr' => json_decode($value, true),
'json-obj' => json_decode($value),
default => $value,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment