diff --git a/README.markdown b/README.markdown index 3d31c8f6f9..d580c27fa0 100644 --- a/README.markdown +++ b/README.markdown @@ -559,17 +559,19 @@ _**Description**_: Get or Set the Redis server configuration parameters. ##### *Prototype* ~~~php -$redis->config($operation, ?string $key = NULL, ?string $value = NULL): mixed; +$redis->config(string $operation, string|array|null $key = NULL, ?string $value = NULL): mixed; ~~~ ##### *Return value* -*Associative array* for `GET`, key -> value -*bool* for `SET`, and `RESETSTAT` +*Associative array* for `GET`, key(s) -> value(s) +*bool* for `SET`, `RESETSTAT`, and `REWRITE` ##### *Examples* ~~~php $redis->config("GET", "*max-*-entries*"); +$redis->config("SET", ['timeout', 'loglevel']); $redis->config("SET", "dir", "/var/run/redis/dumps/"); +$redis->config("SET", ['timeout' => 128, 'loglevel' => 'warning']); $redis->config('RESETSTAT'); ~~~ diff --git a/library.c b/library.c index bbdca77d25..3c8f5c4dae 100644 --- a/library.c +++ b/library.c @@ -1151,6 +1151,15 @@ PHP_REDIS_API int redis_type_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *r return SUCCESS; } +PHP_REDIS_API int +redis_config_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) { + FailableResultCallback cb = ctx; + + ZEND_ASSERT(cb == redis_boolean_response || cb == redis_mbulk_reply_zipped_raw); + + return cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx); +} + PHP_REDIS_API int redis_info_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) { char *response; int response_len; diff --git a/library.h b/library.h index b67d72f876..036cb45615 100644 --- a/library.h +++ b/library.h @@ -67,6 +67,7 @@ PHP_REDIS_API int redis_bulk_double_response(INTERNAL_FUNCTION_PARAMETERS, Redis PHP_REDIS_API int redis_string_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API int redis_ping_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API int redis_info_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); +PHP_REDIS_API int redis_config_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API void redis_parse_info_response(char *response, zval *z_ret); PHP_REDIS_API void redis_parse_client_list_response(char *response, zval *z_ret); PHP_REDIS_API int redis_type_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); diff --git a/redis.c b/redis.c index 9ce8018b21..788deff35a 100644 --- a/redis.c +++ b/redis.c @@ -2707,45 +2707,10 @@ PHP_METHOD(Redis, setOption) /* }}} */ /* {{{ proto boolean Redis::config(string op, string key [, mixed value]) */ -PHP_METHOD(Redis, config) -{ - zend_string *op, *key = NULL, *val = NULL; - FailableResultCallback cb; - RedisSock *redis_sock; - zval *object; - int cmd_len; - char *cmd; - - if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "OS|SS", &object, - redis_ce, &op, &key, &val) == FAILURE) - { - RETURN_FALSE; - } - - if ((redis_sock = redis_sock_get(object, 0)) == NULL) { - RETURN_FALSE; - } - - if (zend_string_equals_literal_ci(op, "GET") && key != NULL) { - cmd_len = REDIS_SPPRINTF(&cmd, "CONFIG", "SS", op, key); - cb = redis_mbulk_reply_zipped_raw; - } else if (zend_string_equals_literal_ci(op, "RESETSTAT") || - zend_string_equals_literal_ci(op, "REWRITE")) - { - cmd_len = REDIS_SPPRINTF(&cmd, "CONFIG", "s", ZSTR_VAL(op), ZSTR_LEN(op)); - cb = redis_boolean_response; - } else if (zend_string_equals_literal_ci(op, "SET") && key != NULL && val != NULL) { - cmd_len = REDIS_SPPRINTF(&cmd, "CONFIG", "SSS", op, key, val); - cb = redis_boolean_response; - } else { - RETURN_FALSE; - } - - REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len) - if (IS_ATOMIC(redis_sock)) { - cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); - } - REDIS_PROCESS_RESPONSE(redis_boolean_response); +/* {{{ proto public function config(string $op, string ...$args) }}} */ +// CONFIG SET/GET +PHP_METHOD(Redis, config) { + REDIS_PROCESS_CMD(config, redis_config_response); } /* }}} */ diff --git a/redis.stub.php b/redis.stub.php index b41ff83101..61ceca0ddb 100644 --- a/redis.stub.php +++ b/redis.stub.php @@ -81,7 +81,29 @@ public function close(): bool; public function command(string $opt = null, string|array $arg): mixed; - public function config(string $operation, ?string $key = NULL, mixed $value = null): mixed; + /** + Execute the Redis CONFIG command in a variety of ways. What the command does in particular depends + on the `$operation` qualifier. + + Operations that PhpRedis supports are: RESETSTAT, REWRITE, GET, and SET. + + @param string $operation The CONFIG subcommand to execute + @param array|string|null $key_or_setting Can either be a setting string for the GET/SET operation or + an array of settings or settings and values. + Note: Redis 7.0.0 is required to send an array of settings. + @param ?string $value The setting value when the operation is SET. + + + config('GET', 'timeout'); + $redis->config('GET', ['timeout', 'databases']); + + $redis->config('SET', 'timeout', 30); + $redis->config('SET', ['timeout' => 30, 'loglevel' => 'warning']); + ?> + + */ + public function config(string $operation, array|string|null $key_or_settings = NULL, ?string $value = NULL): mixed; public function connect(string $host, int $port = 6379, float $timeout = 0, string $persistent_id = null, int $retry_interval = 0, float $read_timeout = 0, array $context = null): bool; diff --git a/redis_arginfo.h b/redis_arginfo.h index 41eb4738ae..d59539d434 100644 --- a/redis_arginfo.h +++ b/redis_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: bd81918bd558ec53399e7f64647c39f288f3413e */ + * Stub hash: a024c59eff58030ac224fc22cc4040b6e926a643 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "null") @@ -127,8 +127,8 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_config, 0, 1, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, operation, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, key, IS_STRING, 1, "NULL") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, value, IS_MIXED, 0, "null") + ZEND_ARG_TYPE_MASK(0, key_or_settings, MAY_BE_ARRAY|MAY_BE_STRING|MAY_BE_NULL, "NULL") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, value, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_connect, 0, 1, _IS_BOOL, 0) diff --git a/redis_commands.c b/redis_commands.c index e415ae5c1e..801e5059b8 100644 --- a/redis_commands.c +++ b/redis_commands.c @@ -728,6 +728,127 @@ int redis_zrangebyscore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return SUCCESS; } +static int redis_build_config_get_cmd(smart_string *dst, zval *val) { + zend_string *zstr; + int ncfg; + zval *zv; + + if (val == NULL || (Z_TYPE_P(val) != IS_STRING && Z_TYPE_P(val) != IS_ARRAY)) { + php_error_docref(NULL, E_WARNING, "Must pass a string or array of values to CONFIG GET"); + return FAILURE; + } else if (Z_TYPE_P(val) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(val)) == 0) { + php_error_docref(NULL, E_WARNING, "Cannot pass an empty array to CONFIG GET"); + return FAILURE; + } + + ncfg = Z_TYPE_P(val) == IS_STRING ? 1 : zend_hash_num_elements(Z_ARRVAL_P(val)); + + REDIS_CMD_INIT_SSTR_STATIC(dst, 1 + ncfg, "CONFIG"); + REDIS_CMD_APPEND_SSTR_STATIC(dst, "GET"); + + if (Z_TYPE_P(val) == IS_STRING) { + redis_cmd_append_sstr_zstr(dst, Z_STR_P(val)); + } else { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(val), zv) { + ZVAL_DEREF(zv); + + zstr = zval_get_string(zv); + redis_cmd_append_sstr_zstr(dst, zstr); + zend_string_release(zstr); + } ZEND_HASH_FOREACH_END(); + } + + return SUCCESS; +} + +static int redis_build_config_set_cmd(smart_string *dst, zval *key, zend_string *val) { + zend_string *zkey, *zstr; + zval *zv; + + /* Legacy case: CONFIG SET */ + if (key != NULL && val != NULL) { + REDIS_CMD_INIT_SSTR_STATIC(dst, 3, "CONFIG"); + REDIS_CMD_APPEND_SSTR_STATIC(dst, "SET"); + + zstr = zval_get_string(key); + redis_cmd_append_sstr_zstr(dst, zstr); + zend_string_release(zstr); + + redis_cmd_append_sstr_zstr(dst, val); + + return SUCCESS; + } + + /* Now we must have an array with at least one element */ + if (key == NULL || Z_TYPE_P(key) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(key)) == 0) { + php_error_docref(NULL, E_WARNING, "Must either pass two strings to CONFIG SET or a non-empty array of values"); + return FAILURE; + } + + REDIS_CMD_INIT_SSTR_STATIC(dst, 1 + (2 * zend_hash_num_elements(Z_ARRVAL_P(key))), "CONFIG"); + REDIS_CMD_APPEND_SSTR_STATIC(dst, "SET"); + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(key), zkey, zv) { + if (zkey == NULL) + goto fail; + + ZVAL_DEREF(zv); + + redis_cmd_append_sstr_zstr(dst, zkey); + + zstr = zval_get_string(zv); + redis_cmd_append_sstr_zstr(dst, zstr); + zend_string_release(zstr); + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; + +fail: + php_error_docref(NULL, E_WARNING, "Must pass an associate array of config keys and values"); + efree(dst->c); + memset(dst, 0, sizeof(*dst)); + return FAILURE; +} + +int +redis_config_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char **cmd, int *cmd_len, short *slot, void **ctx) +{ + zend_string *op = NULL, *arg = NULL; + smart_string cmdstr = {0}; + int res = FAILURE; + zval *key = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STR(op) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(key) + Z_PARAM_STR_OR_NULL(arg) + ZEND_PARSE_PARAMETERS_END_EX(return FAILURE); + + if (zend_string_equals_literal_ci(op, "RESETSTAT") || + zend_string_equals_literal_ci(op, "REWRITE")) + { + REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1, "CONFIG"); + redis_cmd_append_sstr_zstr(&cmdstr, op); + *ctx = redis_boolean_response; + res = SUCCESS; + } else if (zend_string_equals_literal_ci(op, "GET")) { + res = redis_build_config_get_cmd(&cmdstr, key); + *ctx = redis_mbulk_reply_zipped_raw; + } else if (zend_string_equals_literal_ci(op, "SET")) { + res = redis_build_config_set_cmd(&cmdstr, key, arg); + *ctx = redis_boolean_response; + } else { + php_error_docref(NULL, E_WARNING, "Unknown operation '%s'", ZSTR_VAL(op)); + return FAILURE; + } + + *cmd = cmdstr.c; + *cmd_len = cmdstr.len; + return res; +} + int redis_zrandmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx) diff --git a/redis_commands.h b/redis_commands.h index ac63bc4113..ad76835613 100644 --- a/redis_commands.h +++ b/redis_commands.h @@ -110,6 +110,9 @@ int redis_zrangebyscore_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw, char **cmd, int *cmd_len, int *withscores, short *slot, void **ctx); +int redis_config_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char **cmd, int *cmd_len, short *slot, void **ctx); + int redis_zrandmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx); diff --git a/redis_legacy_arginfo.h b/redis_legacy_arginfo.h index 18db4f2584..27aec07433 100644 --- a/redis_legacy_arginfo.h +++ b/redis_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: bd81918bd558ec53399e7f64647c39f288f3413e */ + * Stub hash: a024c59eff58030ac224fc22cc4040b6e926a643 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0) ZEND_ARG_INFO(0, options) @@ -117,7 +117,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_config, 0, 0, 1) ZEND_ARG_INFO(0, operation) - ZEND_ARG_INFO(0, key) + ZEND_ARG_INFO(0, key_or_settings) ZEND_ARG_INFO(0, value) ZEND_END_ARG_INFO() diff --git a/tests/RedisTest.php b/tests/RedisTest.php index c240bc9f72..eab3e2f724 100644 --- a/tests/RedisTest.php +++ b/tests/RedisTest.php @@ -5612,9 +5612,9 @@ public function testConfig() { /* Ensure invalid calls are handled by PhpRedis */ foreach (['notacommand', 'get', 'set'] as $cmd) { - $this->assertFalse($this->redis->config($cmd)); + $this->assertFalse(@$this->redis->config($cmd)); } - $this->assertFalse($this->redis->config('set', 'foo')); + $this->assertFalse(@$this->redis->config('set', 'foo')); /* REWRITE. We don't care if it actually works, just that the command be attempted */ @@ -5624,6 +5624,38 @@ public function testConfig() { $this->assertPatternMatch($this->redis->getLastError(), '/.*config.*/'); $this->redis->clearLastError(); } + + if (!$this->minVersionCheck("7.0.0")) + return; + + /* Test getting multiple values */ + $settings = $this->redis->config('get', ['timeout', 'databases', 'set-max-intset-entries']); + $this->assertTrue(is_array($settings) && isset($settings['timeout']) && + isset($settings['databases']) && isset($settings['set-max-intset-entries'])); + + /* Short circuit if the above assertion would have failed */ + if ( ! is_array($settings) || ! isset($settings['timeout']) || ! isset($settings['set-max-intset-entries'])) + return; + + list($timeout, $max_intset) = [$settings['timeout'], $settings['set-max-intset-entries']]; + + $updates = [ + ['timeout' => (string)($timeout + 30), 'set-max-intset-entries' => (string)($max_intset + 128)], + ['timeout' => (string)($timeout), 'set-max-intset-entries' => (string)$max_intset], + ]; + + foreach ($updates as $update) { + $this->assertTrue($this->redis->config('set', $update)); + $vals = $this->redis->config('get', array_keys($update)); + ksort($vals); + ksort($update); + $this->assertEquals($vals, $update); + } + + /* Make sure PhpRedis catches malformed multiple get/set calls */ + $this->assertFalse(@$this->redis->config('get', [])); + $this->assertFalse(@$this->redis->config('set', [])); + $this->assertFalse(@$this->redis->config('set', [0, 1, 2])); } public function testReconnectSelect() {