diff --git a/README.markdown b/README.markdown index 2106f96e93..68cb9029a9 100644 --- a/README.markdown +++ b/README.markdown @@ -1098,17 +1098,18 @@ $redis->get('x'); // → `FALSE` ----- _**Description**_: Same as rename, but will not replace a key if the destination already exists. This is the same behaviour as setNx. -### expire, setTimeout, pexpire +### expire, pexpire ----- -_**Description**_: Sets an expiration date (a timeout) on an item. pexpire requires a TTL in milliseconds. +_**Description**_: Sets an expiration on a key in either seconds or milliseconds. -##### *Parameters* -*Key*: key. The key that will disappear. - -*Integer*: ttl. The key's remaining Time To Live, in seconds. +##### *Prototype* +~~~php +public function expire(string $key, int $seconds, ?string $mode = NULL): Redis|bool; +public function pexpire(string $key, int $milliseconds, ?string $mode = NULL): Redis|bool; +~~~ ##### *Return value* -*BOOL*: `TRUE` in case of success, `FALSE` in case of failure. +*BOOL*: `TRUE` if an expiration was set, and `FALSE` on failure or if one was not set. You can distinguish between an error and an expiration not being set by checking `getLastError()`. ##### *Example* ~~~php $redis->set('x', '42'); @@ -1121,22 +1122,23 @@ $redis->get('x'); // will return `FALSE`, as 'x' has expired. ### expireAt, pexpireAt ----- -_**Description**_: Sets an expiration date (a timestamp) on an item. pexpireAt requires a timestamp in milliseconds. - -##### *Parameters* -*Key*: key. The key that will disappear. +_**Description**_: Seta specific timestamp for a key to expire in seconds or milliseconds. -*Integer*: Unix timestamp. The key's date of death, in seconds from Epoch time. +##### *Prototype* +~~~php +public function expireat(string $key, int $unix_timestamp, ?string $mode = NULL): Redis|bool; +public function pexpireat(string $key, int $unix_timestamp_millis, ?string $mode = NULL): Redis|bool; +~~~ ##### *Return value* -*BOOL*: `TRUE` in case of success, `FALSE` in case of failure. +*BOOL*: `TRUE` if an expiration was set and `FALSE` if one was not set or in the event on an error. You can detect an actual error by checking `getLastError()`. + ##### *Example* ~~~php $redis->set('x', '42'); -$now = time(NULL); // current timestamp -$redis->expireAt('x', $now + 3); // x will disappear in 3 seconds. +$redis->expireAt('x', time(NULL) + 3); // x will disappear in 3 seconds. sleep(5); // wait 5 seconds -$redis->get('x'); // will return `FALSE`, as 'x' has expired. +$redis->get('x'); // will return `FALSE`, as 'x' has expired. ~~~ ### keys, getKeys diff --git a/redis.c b/redis.c index 3a3370bfac..36b0cf186e 100644 --- a/redis.c +++ b/redis.c @@ -1635,25 +1635,25 @@ PHP_METHOD(Redis, sortDescAlpha) /* {{{ proto array Redis::expire(string key, int timeout) */ PHP_METHOD(Redis, expire) { - REDIS_PROCESS_KW_CMD("EXPIRE", redis_key_long_cmd, redis_1_response); + REDIS_PROCESS_KW_CMD("EXPIRE", redis_expire_cmd, redis_1_response); } /* }}} */ /* {{{ proto bool Redis::pexpire(string key, long ms) */ PHP_METHOD(Redis, pexpire) { - REDIS_PROCESS_KW_CMD("PEXPIRE", redis_key_long_cmd, redis_1_response); + REDIS_PROCESS_KW_CMD("PEXPIRE", redis_expire_cmd, redis_1_response); } /* }}} */ /* {{{ proto array Redis::expireAt(string key, int timestamp) */ PHP_METHOD(Redis, expireAt) { - REDIS_PROCESS_KW_CMD("EXPIREAT", redis_key_long_cmd, redis_1_response); + REDIS_PROCESS_KW_CMD("EXPIREAT", redis_expire_cmd, redis_1_response); } /* }}} */ /* {{{ proto array Redis::pexpireAt(string key, int timestamp) */ PHP_METHOD(Redis, pexpireAt) { - REDIS_PROCESS_KW_CMD("PEXPIREAT", redis_key_long_cmd, redis_1_response); + REDIS_PROCESS_KW_CMD("PEXPIREAT", redis_expire_cmd, redis_1_response); } /* }}} */ diff --git a/redis.stub.php b/redis.stub.php index 8cdede9564..804b0cb54f 100644 --- a/redis.stub.php +++ b/redis.stub.php @@ -105,9 +105,32 @@ public function exec(): Redis|array|false; public function exists(mixed $key, mixed ...$other_keys): Redis|int|bool; - public function expire(string $key, int $timeout): Redis|bool; + /** + Sets an expiration in seconds on the key in question. If connected to + redis-server >= 7.0.0 you may send an additional "mode" argument which + modifies how the command will execute. + + @param string $key The key to set an expiration on. + @param ?string $mode A two character modifier that changes how the + command works. + NX - Set expiry only if key has no expiry + XX - Set expiry only if key has an expiry + LT - Set expiry only when new expiry is < current expiry + GT - Set expiry only when new expiry is > current expiry + */ + public function expire(string $key, int $timeout, ?string $mode = NULL): Redis|bool; + + /** + Set a key's expiration to a specific unix timestamp in seconds. If + connected to Redis >= 7.0.0 you can pass an optional 'mode' argument. - public function expireAt(string $key, int $timestamp): Redis|bool; + @see Redis::expire() For a description of the mode argument. + + @param string $key The key to set an expiration on. + @param ?string $mode A two character modifier that changes how the + command works. + */ + public function expireAt(string $key, int $timestamp, ?string $mode = NULL): Redis|bool; public function failover(?array $to = null, bool $abort = false, int $timeout = 0): Redis|bool; @@ -313,11 +336,32 @@ public function open(string $host, int $port = 6379, float $timeout = 0, string public function pconnect(string $host, int $port = 6379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool; -public function persist(string $key): bool; + public function persist(string $key): bool; + + /** + Sets an expiration in milliseconds on a given key. If connected to + Redis >= 7.0.0 you can pass an optional mode argument that modifies + how the command will execute. + + @see Redis::expire() for a description of the mode argument. - public function pexpire(string $key, int $timeout): bool; + @param string $key The key to set an expiration on. + @param ?string $mode A two character modifier that changes how the + command works. + */ + public function pexpire(string $key, int $timeout, ?string $mode = NULL): bool; - public function pexpireAt(string $key, int $timestamp): bool; + /** + Set a key's expiration to a specific unix timestamp in milliseconds. If + connected to Redis >= 7.0.0 you can pass an optional 'mode' argument. + + @see Redis::expire() For a description of the mode argument. + + @param string $key The key to set an expiration on. + @param ?string $mode A two character modifier that changes how the + command works. + */ + public function pexpireAt(string $key, int $timestamp, ?string $mode = NULL): bool; public function pfadd(string $key, array $elements): int; diff --git a/redis_arginfo.h b/redis_arginfo.h index 37241bbf0c..3c711f3aba 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: 1810caef11b38440e073059e2d9c65f92fa8a9a5 */ + * Stub hash: b53146f6b329f404b4bfa9e5df9dde9c36b50440 */ 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") @@ -201,11 +201,13 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_expire, 0, 2, Redis, MAY_BE_BOOL) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timeout, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_expireAt, 0, 2, Redis, MAY_BE_BOOL) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timestamp, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_failover, 0, 0, Redis, MAY_BE_BOOL) @@ -287,7 +289,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_geosearchstore, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_get, 0, 1, Redis, MAY_BE_ANY|MAY_BE_FALSE) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_get, 0, 1, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -576,11 +578,13 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_pexpire, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timeout, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_pexpireAt, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timestamp, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Redis_pfadd, 0, 2, IS_LONG, 0) diff --git a/redis_cluster.c b/redis_cluster.c index cdd730cefa..0a701ed8bb 100644 --- a/redis_cluster.c +++ b/redis_cluster.c @@ -1252,24 +1252,24 @@ PHP_METHOD(RedisCluster, decrbyfloat) { /* {{{ proto bool RedisCluster::expire(string key, long sec) */ PHP_METHOD(RedisCluster, expire) { - CLUSTER_PROCESS_KW_CMD("EXPIRE", redis_key_long_cmd, cluster_1_resp, 0); + CLUSTER_PROCESS_KW_CMD("EXPIRE", redis_expire_cmd, cluster_1_resp, 0); } /* }}} */ /* {{{ proto bool RedisCluster::expireat(string key, long ts) */ PHP_METHOD(RedisCluster, expireat) { - CLUSTER_PROCESS_KW_CMD("EXPIREAT", redis_key_long_cmd, cluster_1_resp, 0); + CLUSTER_PROCESS_KW_CMD("EXPIREAT", redis_expire_cmd, cluster_1_resp, 0); } /* {{{ proto bool RedisCluster::pexpire(string key, long ms) */ PHP_METHOD(RedisCluster, pexpire) { - CLUSTER_PROCESS_KW_CMD("PEXPIRE", redis_key_long_cmd, cluster_1_resp, 0); + CLUSTER_PROCESS_KW_CMD("PEXPIRE", redis_expire_cmd, cluster_1_resp, 0); } /* }}} */ /* {{{ proto bool RedisCluster::pexpireat(string key, long ts) */ PHP_METHOD(RedisCluster, pexpireat) { - CLUSTER_PROCESS_KW_CMD("PEXPIREAT", redis_key_long_cmd, cluster_1_resp, 0); + CLUSTER_PROCESS_KW_CMD("PEXPIREAT", redis_expire_cmd, cluster_1_resp, 0); } /* }}} */ diff --git a/redis_cluster.stub.php b/redis_cluster.stub.php index 90347c9aed..485b5479a4 100644 --- a/redis_cluster.stub.php +++ b/redis_cluster.stub.php @@ -95,9 +95,9 @@ public function exec(): array|false; public function exists(mixed $key, mixed ...$other_keys): RedisCluster|int|bool; - public function expire(string $key, int $timeout): RedisCluster|bool; + public function expire(string $key, int $timeout, ?string $mode = NULL): RedisCluster|bool; - public function expireat(string $key, int $timestamp): RedisCluster|bool; + public function expireat(string $key, int $timestamp, ?string $mode = NULL): RedisCluster|bool; public function expiretime(string $key): RedisCluster|int|false; @@ -218,9 +218,9 @@ public function object(string $subcommand, string $key): RedisCluster|int|string public function persist(string $key): RedisCluster|bool; - public function pexpire(string $key, int $timeout): RedisCluster|bool; + public function pexpire(string $key, int $timeout, ?string $mode = NULL): RedisCluster|bool; - public function pexpireat(string $key, int $timestamp): RedisCluster|bool; + public function pexpireat(string $key, int $timestamp, ?string $mode = NULL): RedisCluster|bool; public function pfadd(string $key, array $elements): RedisCluster|bool; diff --git a/redis_cluster_arginfo.h b/redis_cluster_arginfo.h index 930ed8e658..1775ca200e 100644 --- a/redis_cluster_arginfo.h +++ b/redis_cluster_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: c6326ac0f4a1dc7b6fe920a7358010f1a570832a */ + * Stub hash: 6b41f3c801e587509bc5c7cab302dc6c1e0f7d56 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 1) @@ -203,11 +203,13 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_expire, 0, 2, RedisCluster, MAY_BE_BOOL) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timeout, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_expireat, 0, 2, RedisCluster, MAY_BE_BOOL) ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, timestamp, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_STRING, 1, "NULL") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_expiretime, 0, 1, RedisCluster, MAY_BE_LONG|MAY_BE_FALSE) diff --git a/redis_cluster_legacy_arginfo.h b/redis_cluster_legacy_arginfo.h index 9fde501cf6..d9c82913f6 100644 --- a/redis_cluster_legacy_arginfo.h +++ b/redis_cluster_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: c6326ac0f4a1dc7b6fe920a7358010f1a570832a */ + * Stub hash: 6b41f3c801e587509bc5c7cab302dc6c1e0f7d56 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1) ZEND_ARG_INFO(0, name) @@ -177,11 +177,13 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster_expire, 0, 0, 2) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, timeout) + ZEND_ARG_INFO(0, mode) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster_expireat, 0, 0, 2) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, timestamp) + ZEND_ARG_INFO(0, mode) ZEND_END_ARG_INFO() #define arginfo_class_RedisCluster_expiretime arginfo_class_RedisCluster__prefix diff --git a/redis_commands.c b/redis_commands.c index 3c56f892af..3a49e48a70 100644 --- a/redis_commands.c +++ b/redis_commands.c @@ -5724,6 +5724,42 @@ int redis_xtrim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return SUCCESS; } +// [P]EXPIRE[AT] [NX | XX | GT | LT] +int redis_expire_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, + void **ctx) +{ + zend_string *key = NULL, *mode = NULL; + smart_string cmdstr = {0}; + zend_long timeout = 0; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(key) + Z_PARAM_LONG(timeout) + Z_PARAM_OPTIONAL + Z_PARAM_STR(mode) + ZEND_PARSE_PARAMETERS_END_EX(return FAILURE); + + if (mode != NULL && !(zend_string_equals_literal_ci(mode, "NX") || + zend_string_equals_literal_ci(mode, "XX") || + zend_string_equals_literal_ci(mode, "LT") || + zend_string_equals_literal_ci(mode, "GT"))) + { + php_error_docref(NULL, E_WARNING, "Unknown expiration modifier '%s'", ZSTR_VAL(mode)); + return FAILURE; + } + + redis_cmd_init_sstr(&cmdstr, 2 + (mode != NULL), kw, strlen(kw)); + redis_cmd_append_sstr_key_zstr(&cmdstr, key, redis_sock, slot); + redis_cmd_append_sstr_long(&cmdstr, timeout); + if (mode != NULL) redis_cmd_append_sstr_zstr(&cmdstr, mode); + + *cmd = cmdstr.c; + *cmd_len = cmdstr.len; + + return SUCCESS; +} + int redis_sentinel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw, char **cmd, int *cmd_len, short *slot, void **ctx) diff --git a/redis_commands.h b/redis_commands.h index a93be4a17b..ac63bc4113 100644 --- a/redis_commands.h +++ b/redis_commands.h @@ -354,6 +354,10 @@ int redis_xreadgroup_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, int redis_xtrim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx); +int redis_expire_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, + void **ctx); + int redis_sentinel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char *kw, char **cmd, int *cmd_len, short *slot, void **ctx); diff --git a/redis_legacy_arginfo.h b/redis_legacy_arginfo.h index 66a63b32c2..3fbd3c4f61 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: 1810caef11b38440e073059e2d9c65f92fa8a9a5 */ + * Stub hash: b53146f6b329f404b4bfa9e5df9dde9c36b50440 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0) ZEND_ARG_INFO(0, options) @@ -181,11 +181,13 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_expire, 0, 0, 2) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, timeout) + ZEND_ARG_INFO(0, mode) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_expireAt, 0, 0, 2) ZEND_ARG_INFO(0, key) ZEND_ARG_INFO(0, timestamp) + ZEND_ARG_INFO(0, mode) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_failover, 0, 0, 0) diff --git a/tests/RedisTest.php b/tests/RedisTest.php index e2c8b55942..1e82920c8c 100644 --- a/tests/RedisTest.php +++ b/tests/RedisTest.php @@ -703,6 +703,39 @@ public function testExpireAt() { $this->assertTrue($success); } + function testExpireOptions() { + if (!$this->minVersionCheck('7.0.0')) + return; + + $this->redis->set('eopts', 'value'); + + /* NX -- Only if expiry isn't set so success, then failure */ + $this->assertTrue($this->redis->expire('eopts', 1000, 'NX')); + $this->assertFalse($this->redis->expire('eopts', 1000, 'NX')); + + /* XX -- Only set if the key has an existing expiry */ + $this->assertTrue($this->redis->expire('eopts', 1000, 'XX')); + $this->assertTrue($this->redis->persist('eopts')); + $this->assertFalse($this->redis->expire('eopts', 1000, 'XX')); + + /* GT -- Only set when new expiry > current expiry */ + $this->assertTrue($this->redis->expire('eopts', 200)); + $this->assertTrue($this->redis->expire('eopts', 300, 'GT')); + $this->assertFalse($this->redis->expire('eopts', 100, 'GT')); + + /* LT -- Only set when expiry < current expiry */ + $this->assertTrue($this->redis->expire('eopts', 200)); + $this->assertTrue($this->redis->expire('eopts', 100, 'LT')); + $this->assertFalse($this->redis->expire('eopts', 300, 'LT')); + + /* Sending a nonsensical mode fails without sending a command */ + $this->redis->clearLastError(); + $this->assertFalse(@$this->redis->expire('eopts', 999, 'nonsense')); + $this->assertEquals(NULL, $this->redis->getLastError()); + + $this->redis->del('eopts'); + } + public function testExpiretime() { if(version_compare($this->version, "7.0.0") < 0) { $this->markTestSkipped();