final class SafeScan

Safely scan the Redis keyspace for keys matching a pattern.

This class provides a memory-efficient iterator over keys using the SCAN command, correctly handling the complexity of Redis OPT_PREFIX configuration and Redis Cluster.

The OPT_PREFIX Problem

phpredis has an OPT_PREFIX option that automatically prepends a prefix to keys for most commands (GET, SET, DEL, etc.). However, this creates complexity:

  1. SCAN does NOT auto-prefix the pattern - You must manually include OPT_PREFIX in your SCAN pattern to match keys that were stored with auto-prefixing.

  2. SCAN returns full keys - Keys returned include the OPT_PREFIX as stored in Redis.

  3. DEL DOES auto-prefix - If you pass a SCAN result directly to DEL, phpredis adds OPT_PREFIX again, causing double-prefixing and failed deletions.

Example of the Bug This Class Prevents

OPT_PREFIX = "myapp:"
Stored key in Redis = "myapp:cache:user:1"

// WRONG approach (what broken code does):
$keys = $redis->scan($iter, "myapp:cache:*");  // Returns ["myapp:cache:user:1"]
$redis->del($keys[0]);  // Tries to delete "myapp:myapp:cache:user:1" - FAILS!

// CORRECT approach (what SafeScan does):
$keys = $redis->scan($iter, "myapp:cache:*");  // Returns ["myapp:cache:user:1"]
$strippedKey = substr($keys[0], strlen("myapp:"));  // "cache:user:1"
$redis->del($strippedKey);  // phpredis adds prefix -> deletes "myapp:cache:user:1" - SUCCESS!

Redis Cluster Support

Redis Cluster requires scanning each master node separately because keys are distributed across slots on different nodes. This class handles this automatically:

  • For standard Redis: Uses scan($iter, $pattern, $count)
  • For RedisCluster: Iterates _masters() and uses scan($iter, $node, $pattern, $count)

Usage

This class is designed to be used within a connection pool callback:

$context->withConnection(
    function (RedisConnection $connection) {
        $optPrefix = (string) $connection->getOption(Redis::OPT_PREFIX);
        $safeScan = new SafeScan($connection, $optPrefix);
        foreach ($safeScan->execute('cache:users:*') as $key) {
            // $key is stripped of OPT_PREFIX, safe to use with del(), get(), etc.
        }
    },
    transform: false,
);

Methods

__construct(RedisConnection $connection, string $optPrefix)

Create a new safe scan instance.

execute(string $pattern, int $count = 1000)

Execute the scan operation.

Details

at line 81
__construct(RedisConnection $connection, string $optPrefix)

Create a new safe scan instance.

Parameters

RedisConnection $connection

The Redis connection (with transform: false for raw phpredis semantics)

string $optPrefix

The OPT_PREFIX value (from $connection->getOption(Redis::OPT_PREFIX))

at line 101
Generator execute(string $pattern, int $count = 1000)

Execute the scan operation.

Parameters

string $pattern

The pattern to match (e.g., "cache:users:*"). Should NOT include OPT_PREFIX - it will be added automatically.

int $count

The COUNT hint for SCAN (not a limit, just a hint to Redis)

Return Value

Generator

yields keys with OPT_PREFIX stripped, safe for use with other phpredis commands that auto-add the prefix