Compare commits

...

16 Commits

Author SHA1 Message Date
Till Krüss 07105e0506 tag v2.4.1 2025-11-12 10:00:11 -08:00
Vladyslav Vildanov 07dc6ba6d2 fix(conn): Removed automatic conn_uid assignment (#1551)
* fix(conn): Removed automatic conn_uid assignment

* Restrict redis version

* Updated CHANGELOG.md

* Added test case

* Update CHANGELOG.md

---------

Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
2025-06-05 09:54:27 +03:00
Till Krüss 5142011581 Update CHANGELOG.md 2025-05-27 09:55:55 -07:00
Tim Düsterhus c4b9c34d86 Fix return type for ZCOUNT (#1546)
* Fix return type for ZCOUNT

ZCOUNT returns an `int`:

> Integer reply: the number of members in the specified score range.

* Update CHANGELOG.md

---------

Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
2025-05-27 10:39:05 +03:00
Till Krüss f49e13ee3a tag stable 2.4.0 2025-04-30 08:16:02 -07:00
Vladyslav Vildanov 6e3e2c0e78 [2.x] 8.0-RC2 testing (#1532) 2025-04-29 08:25:13 -07:00
Vladyslav Vildanov c35c422eda [2.x] Fix the problem of protocol loss during cluster MOVED/ASK (#1530)
Co-authored-by: Lynn Club <lynncho@live.com>
Co-authored-by: Lynn Club <10293848+lynnclub@users.noreply.github.com>
Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
2025-04-17 08:49:19 -07:00
Till Krüss 1b5ed7d516 ZPOPMIN & ZPOPMAX prefix tests 2025-04-15 13:25:42 -07:00
vladvildanov 5df852f227 Added tests cases 2025-04-15 09:16:08 -07:00
Till Krüss 9418df1924 fix changelog branch check 2025-04-15 09:15:52 -07:00
Till Krüss 0e4b3829a7 fix changelog check 2025-04-15 09:07:26 -07:00
Till Krüss 65f6127fab add changelog check 2025-04-15 08:54:08 -07:00
Till Krüss 7ff24b19ae Merge pull request #1528 from predis/vv-2.x-readme-8.0-support
(2.x) Update README.md (Redis 8.0 support)
2025-04-14 09:11:07 -07:00
vladvildanov 2babfc91d7 Update README.md (Redis 8.0 support) 2025-04-14 18:42:11 +03:00
Till Krüss fdd52d0284 Tag v2.4.0-RC1 (#1523) 2025-03-24 10:07:15 -07:00
Vladyslav Vildanov 48a861053e Added new hash-field expiration commands (#1520)
* Added new hash-field expiration commands

* Changed argument default value

* Marked test as skipped

* Removed version restriction

* Updated CHANGELOG.md
2025-03-24 17:50:12 +02:00
22 changed files with 980 additions and 17 deletions
+29
View File
@@ -165,3 +165,32 @@ jobs:
- name: Search for misspellings
run: $(python -m site --user-base)/bin/codespell
changelog:
name: Changelog
runs-on: ubuntu-latest
timeout-minutes: 5
if: >-
github.event_name == 'pull_request' &&
!contains(github.event.head_commit.message, 'nochangelog') &&
!contains(github.event.head_commit.message, 'no-changelog') &&
!contains(github.event.head_commit.message, 'no changelog') &&
!contains(github.event.pull_request.labels.*.name, 'no-changelog')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for CHANGELOG entry
env:
TARGET: ${{ github.event.pull_request.base.ref }}
run: |
FILES_CHANGED=$(git diff --name-only origin/$TARGET...HEAD | grep -E 'CHANGELOG\.md' -c)
if [ "$FILES_CHANGED" != "1" ]; then
echo "CHANGELOG.md was not updated";
exit 1;
fi;
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
run: |
# Mapping of original redis versions to client test containers
declare -A redis_clients_version_mapping=(
["8.0"]="8.0-M04-pre"
["8.0"]="8.0-RC2-pre"
["7.4"]="7.4.2"
["7.2"]="7.2.7"
["6.2"]="6.2.17"
+24 -2
View File
@@ -1,8 +1,30 @@
## Changelog
## Unreleased
## v2.4.1 (2025-11-12)
### Fixed
- Fixed PHP 8.4 compatibility with `stream_context_set_option()`
- Fixed return type for `ZCOUNT` to be `int` (#1546)
- Removed automatic `conn_uid` parameter assignment (#1551)
## v2.4.0 (2025-04-30)
### Added
- Added new hash-field expiration commands (#1520)
- Added missing `FT._LIST` and `BITFIELD_RO` commands (#1521)
### Changed
- Update `WATCH` command to accept `string|string[]` (#1476)
- Optimize cluster slotmap with compact slot range object (#1493)
### Fixed
- Fixed `EVAL_RO` cluster support (#1449)
- Fixed PHP 8.4 compatibility with `stream_context_set_option()` (#1503)
- Prevent named arguments runtime failure (#1509)
- Mark `GEOSEARCH` as read-only to ensure execution on replica (#1481)
- Fixed protocol loss during redis cluster `MOVED` / `ASK` (#1530)
### Maintenance
- Added CI testing with Redis 8.0 (#1510)
- Added test coverage for compatibility with Redis 8.0 (#1513)
- Use parallel on PHP-CS-Fixer (#1489)
## v2.3.0 (2024-11-21)
### Added
+45 -1
View File
@@ -14,7 +14,7 @@ More details about this project can be found on the [frequently asked questions]
## Main features ##
- Support for Redis from __3.0__ to __7.4__.
- Support for Redis from __3.0__ to __8.0__.
- Support for clustering using client-side sharding and pluggable keyspace distributors.
- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel).
@@ -138,6 +138,50 @@ it is still desired to have control of when the connection is opened or closed:
achieved by invoking `$client->connect()` and `$client->disconnect()`. Please note that the effect
of these methods on aggregate connections may differ depending on each specific implementation.
#### Persistent connections ####
To increase a performance of your application you may set up a client to use persistent TCP connection, this way
client saves a time on socket creation and connection handshake. By default, connection is created on first-command
execution and will be automatically closed by GC before the process is being killed.
However, if your application is backed by PHP-FPM the processes are idle, and you may set up it to be persistent and
reusable across multiple script execution within the same process.
To enable the persistent connection mode you should provide following configuration:
```php
// Standalone
$client = new Predis\Client(['persistent' => true]);
// Cluster
$client = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
);
```
**Important**
If you operate on multiple clients within the same application, and they communicate with the same resource, by default
they will share the same socket (that's the default behaviour of persistent sockets). So in this case you would need
to additionally provide a `conn_uid` identifier for each client, this way each client will create its own socket so
the connection context won't be shared across clients. This socket behaviour explained
[here](https://www.php.net/manual/en/function.stream-socket-client.php#105393)
```php
// Standalone
$client1 = new Predis\Client(['persistent' => true, 'conn_uid' => 'id_1']);
$client2 = new Predis\Client(['persistent' => true, 'conn_uid' => 'id_2']);
// Cluster
$client1 = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 'id_1']]
);
$client2 = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 'id_2']]
);
```
### Client configuration ###
+1 -1
View File
@@ -1 +1 @@
2.3.1-dev
2.4.0
+1 -1
View File
@@ -53,7 +53,7 @@ use Traversable;
*/
class Client implements ClientInterface, IteratorAggregate
{
public const VERSION = '2.3.1-dev';
public const VERSION = '2.4.1';
/** @var OptionsInterface */
private $options;
+5
View File
@@ -44,6 +44,8 @@ use Predis\Command\Redis\Container\FunctionContainer;
use Predis\Command\Redis\Container\Json\JSONDEBUG;
use Predis\Command\Redis\Container\Search\FTCONFIG;
use Predis\Command\Redis\Container\Search\FTCURSOR;
use Predis\Command\Redis\HGETEX;
use Predis\Command\Redis\HSETEX;
/**
* Interface defining a client-side context such as a pipeline or transaction.
@@ -164,7 +166,9 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method $this hpexpiretime(string $key, array $fields)
* @method $this hget($key, $field)
* @method $this hgetex(string $key, array $fields, string $modifier = HGETEX::NULL)
* @method $this hgetall($key)
* @method $this hgetdel(string $key, array $fields)
* @method $this hincrby($key, $field, $increment)
* @method $this hincrbyfloat($key, $field, $increment)
* @method $this hkeys($key)
@@ -174,6 +178,7 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method $this hscan($key, $cursor, ?array $options = null)
* @method $this hset($key, $field, $value)
* @method $this hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method $this hsetnx($key, $field, $value)
* @method $this httl(string $key, array $fields)
* @method $this hpttl(string $key, array $fields)
+6 -1
View File
@@ -45,6 +45,8 @@ use Predis\Command\Redis\Container\FunctionContainer;
use Predis\Command\Redis\Container\Json\JSONDEBUG;
use Predis\Command\Redis\Container\Search\FTCONFIG;
use Predis\Command\Redis\Container\Search\FTCURSOR;
use Predis\Command\Redis\HGETEX;
use Predis\Command\Redis\HSETEX;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Response\Status;
@@ -173,7 +175,9 @@ use Predis\Response\Status;
* @method array|null hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method array|null hpexpiretime(string $key, array $fields)
* @method string|null hget(string $key, string $field)
* @method array|null hgetex(string $key, array $fields, string $modifier = HGETEX::NULL, int|bool $modifierValue = false)
* @method array hgetall(string $key)
* @method array hgetdel(string $key, array $fields)
* @method int hincrby(string $key, string $field, int $increment)
* @method string hincrbyfloat(string $key, string $field, int|float $increment)
* @method array hkeys(string $key)
@@ -183,6 +187,7 @@ use Predis\Response\Status;
* @method array hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method array hscan(string $key, $cursor, ?array $options = null)
* @method int hset(string $key, string $field, string $value)
* @method int hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method int hsetnx(string $key, string $field, string $value)
* @method array|null httl(string $key, array $fields)
* @method array|null hpttl(string $key, array $fields)
@@ -295,7 +300,7 @@ use Predis\Response\Status;
* @method array|null xread(int $count = null, int $block = null, array $streams = null, string ...$id)
* @method int zadd(string $key, array $membersAndScoresDictionary)
* @method int zcard(string $key)
* @method string zcount(string $key, int|string $min, int|string $max)
* @method int zcount(string $key, int|string $min, int|string $max)
* @method array zdiff(array $keys, bool $withScores = false)
* @method int zdiffstore(string $destination, array $keys)
* @method string zincrby(string $key, int $increment, string $member)
+35
View File
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
class HGETDEL extends RedisCommand
{
public function getId()
{
return 'HGETDEL';
}
/**
* @param array $arguments
* @return void
*/
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0], 'FIELDS', count($arguments[1])];
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class HGETEX extends RedisCommand
{
public const NULL = '';
public const EX = 'ex';
public const PX = 'px';
public const EXAT = 'exat';
public const PXAT = 'pxat';
public const PERSIST = 'persist';
/**
* @var string[]
*/
private static $modifierEnum = [
self::EX => 'EX',
self::PX => 'PX',
self::EXAT => 'EXAT',
self::PXAT => 'PXAT',
self::PERSIST => 'PERSIST',
];
public function getId()
{
return 'HGETEX';
}
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0]];
// Only required arguments
if (!array_key_exists(2, $arguments) || $arguments[2] == '') {
array_push($processedArguments, 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
return;
}
if (!in_array(strtoupper($arguments[2]), self::$modifierEnum)) {
$enumValues = implode(', ', array_keys(self::$modifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
// PERSIST requires no additional value
if (strtoupper($arguments[2]) === self::$modifierEnum['persist']) {
$processedArguments[] = self::$modifierEnum['persist'];
array_push($processedArguments, 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
return;
}
if (!array_key_exists(3, $arguments) || !is_int($arguments[3])) {
throw new UnexpectedValueException('Modifier value is missing or incorrect type');
}
// Order matters so FIELDS should be at the end
array_push($processedArguments, self::$modifierEnum[strtolower($arguments[2])], $arguments[3], 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class HSETEX extends RedisCommand
{
public const TTL_NULL = '';
public const TTL_EX = 'ex';
public const TTL_PX = 'px';
public const TTL_EXAT = 'exat';
public const TTL_PXAT = 'pxat';
public const TTL_KEEP_TTL = 'keepttl';
public const SET_NULL = '';
public const SET_FNX = 'fnx';
public const SET_FXX = 'fxx';
/**
* @var string[]
*/
private static $ttlModifierEnum = [
self::TTL_EX => 'EX',
self::TTL_PX => 'PX',
self::TTL_EXAT => 'EXAT',
self::TTL_PXAT => 'PXAT',
self::TTL_KEEP_TTL => 'KEEPTTL',
];
/**
* @var string[]
*/
private static $setModifierEnum = [
self::SET_FNX => 'FNX',
self::SET_FXX => 'FXX',
];
public function getId()
{
return 'HSETEX';
}
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0]];
$flatArray = [];
// Convert key => value, into key, value
array_walk($arguments[1], function ($value, $key) use (&$flatArray) {
array_push($flatArray, $key, $value);
});
// Only required arguments
if (!array_key_exists(2, $arguments)) {
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if ($arguments[2] !== '') {
if (!in_array(strtoupper($arguments[2]), self::$setModifierEnum)) {
$enumValues = implode(', ', array_keys(self::$setModifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
$processedArguments[] = self::$setModifierEnum[strtolower($arguments[2])];
}
// Required + set modifier
if (!array_key_exists(3, $arguments) || $arguments[3] == '') {
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if (!in_array(strtoupper($arguments[3]), self::$ttlModifierEnum)) {
$enumValues = implode(', ', array_keys(self::$ttlModifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
// KEEPTTL requires no additional value
if (strtoupper($arguments[3]) === self::$ttlModifierEnum[self::TTL_KEEP_TTL]) {
$processedArguments[] = self::$ttlModifierEnum[self::TTL_KEEP_TTL];
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if (!array_key_exists(4, $arguments) || !is_int($arguments[4])) {
throw new UnexpectedValueException('Modifier value is missing or incorrect type');
}
// Order matters so FIELDS should be at the end
array_push($processedArguments, self::$ttlModifierEnum[strtolower($arguments[3])], $arguments[4], 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
}
}
+12 -3
View File
@@ -254,7 +254,7 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
}
if (!$connection = $this->getRandomConnection()) {
throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`');
throw new ClientException('No connections left in the pool for `CLUSTER SLOTS` (' . $exception->getMessage() . ')');
}
usleep($retryAfter * 1000);
@@ -337,10 +337,19 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
{
$separator = strrpos($connectionID, ':');
return $this->connections->create([
$parameters = [
'host' => substr($connectionID, 0, $separator),
'port' => substr($connectionID, $separator + 1),
]);
];
$existConnection = current($this->pool);
if ($existConnection instanceof NodeConnectionInterface) {
$existParameters = $existConnection->getParameters()->toArray();
unset($existParameters['alias'], $existParameters['slots']);
$parameters = array_merge($existParameters, $parameters);
}
return $this->connections->create($parameters);
}
/**
-1
View File
@@ -41,7 +41,6 @@ class StreamConnection extends AbstractConnection
public function __construct(ParametersInterface $parameters)
{
parent::__construct($parameters);
$this->parameters->conn_uid = spl_object_hash($this);
}
/**
+22 -4
View File
@@ -1285,17 +1285,35 @@ class ClientTest extends PredisTestCase
/**
* @group connected
* @requiresRedisVersion >= 5.0.0
*/
public function testClientsCreateDifferentPersistentConnections(): void
{
$client1 = new Client($this->getParameters(['database' => 14, 'persistent' => true]));
$client2 = new Client($this->getParameters(['database' => 15, 'persistent' => true]));
$client1 = new Client($this->getParameters(['database' => 14, 'persistent' => true, 'conn_uid' => 1]));
$client2 = new Client($this->getParameters(['database' => 15, 'persistent' => true, 'conn_uid' => 2]));
$client1->set('foo', 'bar');
$client2->set('foo', 'baz');
$this->assertSame('bar', $client1->get('foo'));
$this->assertSame('baz', $client2->get('foo'));
$this->assertNotSame($client1->client('ID'), $client2->client('ID'));
}
/**
* @group connected
* @requiresRedisVersion >= 5.0.0
*/
public function testClientsCreateSamePersistentConnections(): void
{
$client1 = new Client($this->getParameters(['persistent' => true]));
$client2 = new Client($this->getParameters(['persistent' => true]));
$client1->set('foo', 'bar');
$client2->set('foo', 'baz');
$this->assertSame('baz', $client2->get('foo'));
$this->assertSame($client1->client('ID'), $client2->client('ID'));
}
/**
@@ -1307,11 +1325,11 @@ class ClientTest extends PredisTestCase
{
$client1 = new Client(
$this->getDefaultParametersArray(),
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 1]]
);
$client2 = new Client(
$this->getDefaultParametersArray(),
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 2]]
);
$client1->set('{shard1}foo', 'bar');
@@ -1008,6 +1008,14 @@ class KeyPrefixProcessorTest extends PredisTestCase
['key', 'MAXLEN', 100],
['prefix:key', 'MAXLEN', 100],
],
['ZPOPMIN',
['key'],
['prefix:key'],
],
['ZPOPMAX',
['key'],
['prefix:key'],
],
/* ---------------- Redis 6.2 ---------------- */
['GETDEL',
['key'],
@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
class HGETDEL_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return HGETDEL::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'HGETDEL';
}
/**
* @group disconnected
*/
public function testFilterArguments(): void
{
$command = $this->getCommand();
$command->setArguments(['key', ['field1', 'field2']]);
$this->assertSame(['key', 'FIELDS', 2, 'field1', 'field2'], $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$command = $this->getCommand();
$this->assertSame(0, $command->parseResponse(0));
$this->assertSame(1, $command->parseResponse(1));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testReturnsAndRemoveFieldsFromHash(): void
{
$redis = $this->getClient();
$redis->hset('hashkey', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3');
$this->assertSame(['value1', 'value2'], $redis->hgetdel('hashkey', ['field1', 'field2']));
$this->assertSame(['field3' => 'value3'], $redis->hgetall('hashkey'));
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use UnexpectedValueException;
class HGETEX_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return HGETEX::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'HGETEX';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$command = $this->getCommand();
$this->assertSame(1, $command->parseResponse(1));
}
/**
* @group connected
* @dataProvider hashProvider
* @param array $hash
* @param array $arguments
* @param array $expectedResponse
* @param float $timeout
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testReturnsValueAndSetExpirationTimeForGivenHash(
array $hash,
array $arguments,
array $expectedResponse,
float $timeout
): void {
$redis = $this->getClient();
$redis->hset(...$hash);
$this->assertSame($expectedResponse, $redis->hgetex(...$arguments));
$this->sleep($timeout);
$this->assertSame([], $redis->hgetall('hash_key'));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testRemovesAssociatedTTLFromHash()
{
$redis = $this->getClient();
$redis->hsetex(
'hash_key', ['field1' => 'value1'],
HSETEX::SET_NULL, HSETEX::TTL_EX, 100
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
$redis->hgetex('hash_key', ['field1'], HGETEX::PERSIST);
$this->assertEquals(-1, $redis->hexpiretime('hash_key', ['field1'])[0]);
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedExceptionMessage
* @return void
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $arguments,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->hgetex(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key', ['field1', 'field2']],
['key', 'FIELDS', 2, 'field1', 'field2'],
],
'with EX modifier' => [
['key', ['field1', 'field2'], HGETEX::EX, 10],
['key', 'EX', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PX modifier' => [
['key', ['field1', 'field2'], HGETEX::PX, 10],
['key', 'PX', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with EXAT modifier' => [
['key', ['field1', 'field2'], HGETEX::EXAT, 10],
['key', 'EXAT', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PXAT modifier' => [
['key', ['field1', 'field2'], HGETEX::PXAT, 10],
['key', 'PXAT', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PERSIST modifier' => [
['key', ['field1', 'field2'], HGETEX::PERSIST],
['key', 'PERSIST', 'FIELDS', 2, 'field1', 'field2'],
],
];
}
public function hashProvider(): array
{
return [
'with expiration - EX modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::EX, 1],
['value1', 'value2'],
1.2,
],
'with expiration - PX modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::PX, 100],
['value1', 'value2'],
0.2,
],
'with expiration - EXAT modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::EXAT, time() + 1],
['value1', 'value2'],
2,
],
'with expiration - PXAT modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::PXAT, (time() * 1000) + 100],
['value1', 'value2'],
0.3,
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'with wrong modifier' => [
['key', ['field1', 'field2'], 'wrong', 10],
'Modifier argument accepts only: ex, px, exat, pxat, persist values',
],
'with wrong type value' => [
['key', ['field1', 'field2'], 'ex', true],
'Modifier value is missing or incorrect type',
],
];
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use UnexpectedValueException;
class HSETEX_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return HSETEX::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'HSETEX';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group connected
* @group slow
* @dataProvider hashProvider
* @param array $arguments
* @param int $expectedResponse
* @param float $timeout
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashWithExpiration(
array $arguments,
int $expectedResponse,
float $timeout
): void {
$redis = $this->getClient();
$this->assertSame($expectedResponse, $redis->hsetex(...$arguments));
$this->sleep($timeout);
$this->assertSame([], $redis->hgetall('hash_key'));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldsIfNotExists(): void
{
$redis = $this->getClient();
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
$this->assertSame(
0,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldsOnlyIfExists(): void
{
$redis = $this->getClient();
$this->assertSame(
0,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX)
);
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldRetainingTTLValue(): void
{
$redis = $this->getClient();
$this->assertSame(
1,
$redis->hsetex(
'hash_key',
['field1' => 'value1', 'field2' => 'value2'],
HSETEX::SET_FNX,
HSETEX::TTL_EX,
100
)
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
$this->assertSame(
1,
$redis->hsetex(
'hash_key',
['field1' => 'value1'],
HSETEX::SET_FXX,
HSETEX::TTL_KEEP_TTL
)
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedExceptionMessage
* @return void
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $arguments,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->hsetex(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key', ['field1' => 'value1', 'field2' => 'value2']],
['key', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with FNX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX],
['key', 'FNX', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with FXX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX],
['key', 'FXX', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with EX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EX, 10],
['key', 'EX', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with PX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PX, 10],
['key', 'PX', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with EXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EXAT, 10],
['key', 'EXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with PXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PXAT, 10],
['key', 'PXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with KEEPTTL modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_KEEP_TTL],
['key', 'KEEPTTL', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with combined modifiers' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX, HSETEX::TTL_PXAT, 10],
['key', 'FXX', 'PXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
];
}
public function hashProvider(): array
{
return [
'with EX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EX, 1],
1,
1.2,
],
'with PX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PX, 100],
1,
0.2,
],
'with EXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EXAT, time() + 1],
1,
2,
],
'with PXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PXAT, (time() * 1000) + 100],
1,
0.3,
],
'with combined modifiers' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX, HSETEX::TTL_PX, 100],
1,
0.2,
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'with wrong set modifier' => [
['key', ['field1', 'field2'], 'wrong'],
'Modifier argument accepts only: fnx, fxx values',
],
'with wrong ttl modifier' => [
['key', ['field1', 'field2'], '', 'wrong'],
'Modifier argument accepts only: ex, px, exat, pxat, keepttl values',
],
'with wrong ttl modifier value' => [
['key', ['field1', 'field2'], '', HSETEX::TTL_PXAT, 'wrong'],
'Modifier value is missing or incorrect type',
],
];
}
}
+2
View File
@@ -319,6 +319,8 @@ BUFFER;
*/
public function testExposeSearchInformation(): void
{
$this->markTestSkipped('Skipped due to a bug in 8.0-M05. Should be removed in the next version.');
$redis = $this->getClient();
$this->assertArrayHasKey('search', $redis->info('modules')['Modules']);
@@ -69,7 +69,7 @@ class TSINFO_Test extends PredisCommandTestCase
public function testReturnsInformationAboutGivenTimeSeries(): void
{
$redis = $this->getClient();
$expectedResponse = ['totalSamples', 0, 'memoryUsage', 4239, 'firstTimestamp', 0, 'lastTimestamp', 0,
$expectedResponse = ['totalSamples', 0, 'memoryUsage', 5000, 'firstTimestamp', 0, 'lastTimestamp', 0,
'retentionTime', 60000, 'chunkCount', 1, 'chunkSize', 4096, 'chunkType', 'compressed', 'duplicatePolicy',
'max', 'labels', [['sensor_id', '2'], ['area_id', '32']], 'sourceKey', null, 'rules', [],
'ignoreMaxTimeDiff', 0, 'ignoreMaxValDiff', 0];
@@ -84,7 +84,7 @@ class TSINFO_Test extends PredisCommandTestCase
$redis->tscreate('temperature:2:32', $arguments)
);
$this->assertEquals($expectedResponse, $redis->tsinfo('temperature:2:32'));
$this->assertEqualsWithDelta($expectedResponse, $redis->tsinfo('temperature:2:32'), 1000);
}
public function argumentsProvider(): array
@@ -319,12 +319,14 @@ class RedisClusterTest extends PredisTestCase
->withConsecutive(
[
[
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6383',
],
],
[
[
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6384',
],
@@ -644,6 +646,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '9381',
])
@@ -710,6 +713,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '9381',
])
@@ -1020,6 +1024,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6381',
])
@@ -1108,6 +1113,58 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6381',
])
->willReturn($connection3);
$cluster = new RedisCluster($factory);
$cluster->useClusterSlots(false);
$cluster->add($connection1);
$cluster->add($connection2);
$this->assertSame('foobar', $cluster->executeCommand($command));
$this->assertSame('foobar', $cluster->executeCommand($command));
$this->assertCount(3, $cluster);
}
/**
* @group disconnected
*/
public function testNotTCPMovedResponseWithConnectionNotInPool(): void
{
$movedResponse = new Response\Error('MOVED 1970 127.0.0.1:6381');
$command = $this->getCommandFactory()->create('get', ['node:1001']);
$connection1 = $this->getMockConnection('tls://127.0.0.1:6379');
$connection1
->expects($this->once())
->method('executeCommand')
->with($command)
->willReturn($movedResponse);
$connection2 = $this->getMockConnection('tls://127.0.0.1:6380');
$connection2
->expects($this->never())
->method('executeCommand');
$connection3 = $this->getMockConnection('tls://127.0.0.1:6381');
$connection3
->expects($this->exactly(2))
->method('executeCommand')
->with($command)
->willReturnOnConsecutiveCalls('foobar', 'foobar');
/** @var Connection\FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
->method('create')
->with([
'scheme' => 'tls',
'host' => '127.0.0.1',
'port' => '6381',
])
@@ -1153,6 +1210,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '2001:db8:0:f101::2',
'port' => '6379',
])
@@ -1250,6 +1308,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6380',
])
@@ -403,6 +403,17 @@ class ParametersTest extends PredisTestCase
$this->assertSame($expected, Parameters::parse($uri));
}
/**
* @group disconnected
*/
public function testSetParameters(): void
{
$parameters = new Parameters();
$parameters->property = 'value';
$this->assertEquals('value', $parameters->property);
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //