From 962252d2c57c187181e67bb66da3f27b4698358d Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 5 Jun 2025 06:59:47 +0300 Subject: [PATCH 01/49] Merge commit from fork --- CHANGELOG.md | 1 + src/SocketException.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be56a9439..405ffd69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Yii Framework 2 redis extension Change Log ------------------------ - Bug #270: Prevent null parameter on `mb_strlen` to avoid PHP 8.4 implicity nullable types deprecation (tehmaestro) +- Bug CVE-2025-48493: Prevent logging `AUTH` parameters when `YII_DEBUG` is off (samdark) 2.0.19 February 13, 2025 diff --git a/src/SocketException.php b/src/SocketException.php index d286ad768..c07b2f426 100644 --- a/src/SocketException.php +++ b/src/SocketException.php @@ -15,6 +15,14 @@ */ class SocketException extends Exception { + public function __construct($message = null, $code = 0, \Exception $previous = null) + { + if (!YII_DEBUG) { + $message = preg_replace('~AUTH \S+ \S+~', 'AUTH *** ***', $message); + } + parent::__construct($message, $code, $previous); + } + /** * @return string the user-friendly name of this exception */ From d5b89cb4977b73bc813d3b85a1017337ac64267c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 5 Jun 2025 07:02:45 +0300 Subject: [PATCH 02/49] release version 2.0.20 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 405ffd69a..038ad2428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ Yii Framework 2 redis extension Change Log ========================================== -2.0.20 under development ------------------------- +2.0.20 June 05, 2025 +-------------------- -- Bug #270: Prevent null parameter on `mb_strlen` to avoid PHP 8.4 implicity nullable types deprecation (tehmaestro) - Bug CVE-2025-48493: Prevent logging `AUTH` parameters when `YII_DEBUG` is off (samdark) +- Bug #270: Prevent null parameter on `mb_strlen` to avoid PHP 8.4 implicity nullable types deprecation (tehmaestro) 2.0.19 February 13, 2025 From 0a90a543e06fd9f201511a839602a83e0b2487dc Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 5 Jun 2025 07:02:57 +0300 Subject: [PATCH 03/49] prepare for next release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 038ad2428..e7e13ee02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Yii Framework 2 redis extension Change Log ========================================== +2.0.21 under development +------------------------ + +- no changes in this release. + + 2.0.20 June 05, 2025 -------------------- From bce389eddb45563267c8346b17c5c5a83a034ec9 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Fri, 27 Jun 2025 18:32:29 +0300 Subject: [PATCH 04/49] update tests --- tests/ActiveDataProviderTest.php | 2 +- tests/ActiveRecordTest.php | 12 ++++++------ tests/RedisConnectionTest.php | 2 +- tests/RedisMutexTest.php | 2 +- tests/TestCase.php | 4 ++-- tests/data/ar/OrderItem.php | 1 + tests/data/config.php | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/ActiveDataProviderTest.php b/tests/ActiveDataProviderTest.php index 182375fad..cf7392858 100644 --- a/tests/ActiveDataProviderTest.php +++ b/tests/ActiveDataProviderTest.php @@ -11,7 +11,7 @@ */ class ActiveDataProviderTest extends TestCase { - public function setUp() + public function setUp(): void { parent::setUp(); ActiveRecord::$db = $this->getConnection(); diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 2013a8fd1..121c518be 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -68,7 +68,7 @@ public function getOrderItemWithNullFKmClass() return OrderItemWithNullFK::className(); } - public function setUp() + public function setUp(): void { parent::setUp(); ActiveRecord::$db = $this->getConnection(); @@ -539,10 +539,10 @@ public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $script = $lua->buildOne($query); foreach($expectedStrings as $string) { - $this->assertContains($string, $script); + $this->assertStringContainsString($string, $script); } foreach($unexpectedStrings as $string) { - $this->assertNotContains($string, $script); + $this->assertStringNotContainsString($string, $script); } } @@ -594,10 +594,10 @@ public function testValueEscapingInFindByCondition($filterWithInjection, $expect $script = $lua->buildOne($query); foreach($expectedStrings as $string) { - $this->assertContains($string, $script); + $this->assertStringContainsString($string, $script); } foreach($unexpectedStrings as $string) { - $this->assertNotContains($string, $script); + $this->assertStringNotContainsString($string, $script); } // ensure injected FLUSHALL call did not succeed $query->one(); @@ -657,7 +657,7 @@ public function testStringCompareCondition() public function testFind() { - /* @var $customerClass \yii\db\ActiveRecordInterface */ + /* @var $customerClass \yii\db\ActiveRecordInterface|string */ $customerClass = $this->getCustomerClass(); // find one diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 0cd57277e..7185328b3 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -13,7 +13,7 @@ */ class ConnectionTest extends TestCase { - protected function tearDown() + protected function tearDown(): void { $this->getConnection(false)->configSet('timeout', 0); parent::tearDown(); diff --git a/tests/RedisMutexTest.php b/tests/RedisMutexTest.php index e9a5dddfd..7bd8a4bd7 100644 --- a/tests/RedisMutexTest.php +++ b/tests/RedisMutexTest.php @@ -97,7 +97,7 @@ public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTi } } - protected function setUp() + protected function setUp(): void { parent::setUp(); $databases = TestCase::getParam('databases'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 354b74fac..dbe6dbfab 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -34,7 +34,7 @@ public static function getParam($name, $default = null) * Clean up after test. * By default the application created with [[mockApplication]] will be destroyed. */ - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); $this->destroyApplication(); @@ -86,7 +86,7 @@ protected function destroyApplication() Yii::$container = new Container(); } - protected function setUp() + protected function setUp(): void { $databases = self::getParam('databases'); $params = isset($databases['redis']) ? $databases['redis'] : null; diff --git a/tests/data/ar/OrderItem.php b/tests/data/ar/OrderItem.php index 26c13538e..1549684e1 100644 --- a/tests/data/ar/OrderItem.php +++ b/tests/data/ar/OrderItem.php @@ -46,4 +46,5 @@ public function getItem() { return $this->hasOne(Item::className(), ['id' => 'item_id']); } + } diff --git a/tests/data/config.php b/tests/data/config.php index ac27cedb2..469798451 100644 --- a/tests/data/config.php +++ b/tests/data/config.php @@ -14,7 +14,7 @@ $config = [ 'databases' => [ 'redis' => [ - 'hostname' => 'localhost', + 'hostname' => 'redis', 'port' => 6379, 'database' => 0, 'password' => null, @@ -26,4 +26,4 @@ include(__DIR__ . '/config.local.php'); } -return $config; \ No newline at end of file +return $config; From cb795ed7fccf3b99a36153780cac5e8b88802290 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Fri, 27 Jun 2025 18:33:31 +0300 Subject: [PATCH 05/49] predis conn --- src/PredisConnection.php | 259 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 src/PredisConnection.php diff --git a/src/PredisConnection.php b/src/PredisConnection.php new file mode 100644 index 000000000..5f4f6ace6 --- /dev/null +++ b/src/PredisConnection.php @@ -0,0 +1,259 @@ + PredisConnection::class, + * 'parameters' => [ + * 'tcp://127.0.0.1:26379?timeout=0.100', + * 'tcp://127.0.0.1:26380?timeout=0.100', + * 'tcp://127.0.0.1:26381?timeout=0.100', + * ], + * 'options' => [ + * 'replication' => 'sentinel', + * // значение для 'service' можно получить подключившись к redis + * // через redis-cli -h 127.0.0.1 -p 26379 --pass 'password' + * // и выполнить SENTINEL masters + * 'service' => 'mymaster', + * 'parameters' => [ + * 'password' => 'password', + * 'database' => 10, + * // @see \Predis\Connection\StreamConnection + * 'persistent' => true, // performs the connection asynchronously + * 'async_connect' => true, //the connection asynchronously + * 'read_write_timeout' => 0.1, // timeout of read / write operations + * //'timeout' => 0.1, // @note timeout переопределяется в predis, timeout в строке подключения + * ], + * ], + * ]; + * // redis-cluster + * 'redis' = [ + * 'class' => PredisConnection::class, + * 'parameters' => [ + * 'tcp://127.0.0.1:5380?timeout=0.100', + * 'tcp://127.0.0.1:5381?timeout=0.100', + * 'tcp://127.0.0.1:5382?timeout=0.100', + * ], + * 'options' => [ + * 'cluster' => 'redis' + * 'parameters' => [ + * 'password' => 'password', + * // @see \Predis\Connection\StreamConnection + * 'persistent' => true, // performs the connection asynchronously + * 'async_connect' => true, //the connection asynchronously + * 'read_write_timeout' => 0.1, // timeout of read / write operations + * //'timeout' => 0.1, // @note timeout переопределяется в predis, используй timeout в строке подключения + * ], + * ]; + * ``` + */ +class PredisConnection extends YiiRedisConnection +{ + /** + * @internal should not be modified from outside + * @deprecated + */ + public $scheme; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $hostname; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $redirectConnectionString; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $unixSocket; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $username; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $password; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $database; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $connectionTimeout; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $dataTimeout; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $useSSL; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $contextOptions; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $socketClientFlags; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $retries; + /** + * @internal should not be modified from outside + * @deprecated + */ + public $retryInterval; + + /** + * @var mixed Connection parameters for one or more servers. + */ + public mixed $parameters; + + /** + * @var mixed Options to configure some behaviours of the client. + */ + public mixed $options = []; + + /** + * @var Client|null redis connection + */ + protected Client|null $clientSocket = null; + + /** + * Returns a value indicating whether the DB connection is established. + * + * @return bool whether the DB connection is established + */ + public function getIsActive(): bool + { + return (bool)$this->clientSocket?->isConnected(); + } + + /** + * @inheritdoc + * @return mixed|ErrorInterface|ResponseInterface + * @throws InvalidConfigException + */ + public function executeCommand($name, $params = []) + { + $this->open(); + + Yii::debug("Executing Redis Command: {$name} " . implode(' ', $params), __METHOD__); + +// $aaa = $this->database; +// $this->clientSocket->select(1); + + $res = $this->clientSocket->executeCommand($this->clientSocket->createCommand($name, $params)); + return $res; + } + + /** + * Establishes a DB connection. + * + * @return void + * @throws InvalidConfigException + */ + public function open(): void + { + if (null !== $this->clientSocket) { + return; + } + + if (empty($this->parameters)) { + throw new InvalidConfigException('Connection::parameters cannot be empty'); + } + + Yii::debug('Opening redis DB connection', __METHOD__); + + $otp = array_merge([ + 'commands' => new YiiCommandFactory(), + $this->options, + ]); + + $this->clientSocket = new Client($this->parameters, $otp); + $this->initConnection(); + } + + /** + * @inheritdoc + */ + public function close(): void + { + $this->clientSocket?->disconnect(); + } + + /** + * Get predis Client + * + * @return Client|null + * @throws InvalidConfigException + */ + public function getClientSocket(): ?Client + { + $this->open(); + + return $this->clientSocket; + } + + /** + * @inheritdoc + */ + public function ping($message = null): bool + { + $this->open(); + return (string)$this->clientSocket->ping() === 'PONG'; + } + + /** + * Allows issuing all supported commands via magic methods. + * ```php + * $redis->hmset('test_collection', 'key1', 'val1', 'key2', 'val2') + * ``` + * + * @param string $name name of the missing method to execute + * @param array $params method call arguments + * @return mixed + */ + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands)) { + $res = $this->executeCommand($redisCommand, $params); + return $res; + } + + return parent::__call($name, $params); + } +} From 5dfa1cfad0a84740745391daf9572c77e68a024a Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 1 Jul 2025 10:57:00 +0300 Subject: [PATCH 06/49] predis standalone and sentinel --- composer.json | 6 +- src/Cache.php | 2 +- src/predis/Command/CommandDecorator.php | 35 + src/predis/Command/HashGetAllCommand.php | 18 + src/predis/CommandFactory.php | 21 + src/{ => predis}/PredisConnection.php | 90 +-- tests/docker/docker-compose.yml | 45 +- .../sentinel/ActiveDataProviderTest.php | 37 + tests/predis/sentinel/ActiveRecordTest.php | 689 ++++++++++++++++++ tests/predis/sentinel/RedisCacheTest.php | 223 ++++++ tests/predis/sentinel/RedisConnectionTest.php | 228 ++++++ tests/predis/sentinel/RedisMutexTest.php | 149 ++++ tests/predis/sentinel/RedisSessionTest.php | 85 +++ tests/predis/sentinel/TestCase.php | 143 ++++ tests/predis/sentinel/UniqueValidatorTest.php | 155 ++++ tests/predis/sentinel/config/config.php | 29 + .../predis/sentinel/data/ar/ActiveRecord.php | 27 + tests/predis/sentinel/data/ar/Customer.php | 105 +++ .../predis/sentinel/data/ar/CustomerQuery.php | 21 + tests/predis/sentinel/data/ar/Item.php | 21 + tests/predis/sentinel/data/ar/Order.php | 159 ++++ tests/predis/sentinel/data/ar/OrderItem.php | 53 ++ .../sentinel/data/ar/OrderItemWithNullFK.php | 30 + .../sentinel/data/ar/OrderWithNullFK.php | 30 + .../standalone/ActiveDataProviderTest.php | 37 + tests/predis/standalone/ActiveRecordTest.php | 689 ++++++++++++++++++ tests/predis/standalone/RedisCacheTest.php | 222 ++++++ .../predis/standalone/RedisConnectionTest.php | 228 ++++++ tests/predis/standalone/RedisMutexTest.php | 149 ++++ tests/predis/standalone/RedisSessionTest.php | 85 +++ tests/predis/standalone/TestCase.php | 143 ++++ .../predis/standalone/UniqueValidatorTest.php | 155 ++++ tests/predis/standalone/config/config.php | 27 + .../standalone/data/ar/ActiveRecord.php | 27 + tests/predis/standalone/data/ar/Customer.php | 105 +++ .../standalone/data/ar/CustomerQuery.php | 21 + tests/predis/standalone/data/ar/Item.php | 21 + tests/predis/standalone/data/ar/Order.php | 159 ++++ tests/predis/standalone/data/ar/OrderItem.php | 53 ++ .../data/ar/OrderItemWithNullFK.php | 30 + .../standalone/data/ar/OrderWithNullFK.php | 30 + 41 files changed, 4496 insertions(+), 86 deletions(-) create mode 100644 src/predis/Command/CommandDecorator.php create mode 100644 src/predis/Command/HashGetAllCommand.php create mode 100644 src/predis/CommandFactory.php rename src/{ => predis}/PredisConnection.php (71%) create mode 100644 tests/predis/sentinel/ActiveDataProviderTest.php create mode 100644 tests/predis/sentinel/ActiveRecordTest.php create mode 100644 tests/predis/sentinel/RedisCacheTest.php create mode 100644 tests/predis/sentinel/RedisConnectionTest.php create mode 100644 tests/predis/sentinel/RedisMutexTest.php create mode 100644 tests/predis/sentinel/RedisSessionTest.php create mode 100644 tests/predis/sentinel/TestCase.php create mode 100644 tests/predis/sentinel/UniqueValidatorTest.php create mode 100644 tests/predis/sentinel/config/config.php create mode 100644 tests/predis/sentinel/data/ar/ActiveRecord.php create mode 100644 tests/predis/sentinel/data/ar/Customer.php create mode 100644 tests/predis/sentinel/data/ar/CustomerQuery.php create mode 100644 tests/predis/sentinel/data/ar/Item.php create mode 100644 tests/predis/sentinel/data/ar/Order.php create mode 100644 tests/predis/sentinel/data/ar/OrderItem.php create mode 100644 tests/predis/sentinel/data/ar/OrderItemWithNullFK.php create mode 100644 tests/predis/sentinel/data/ar/OrderWithNullFK.php create mode 100644 tests/predis/standalone/ActiveDataProviderTest.php create mode 100644 tests/predis/standalone/ActiveRecordTest.php create mode 100644 tests/predis/standalone/RedisCacheTest.php create mode 100644 tests/predis/standalone/RedisConnectionTest.php create mode 100644 tests/predis/standalone/RedisMutexTest.php create mode 100644 tests/predis/standalone/RedisSessionTest.php create mode 100644 tests/predis/standalone/TestCase.php create mode 100644 tests/predis/standalone/UniqueValidatorTest.php create mode 100644 tests/predis/standalone/config/config.php create mode 100644 tests/predis/standalone/data/ar/ActiveRecord.php create mode 100644 tests/predis/standalone/data/ar/Customer.php create mode 100644 tests/predis/standalone/data/ar/CustomerQuery.php create mode 100644 tests/predis/standalone/data/ar/Item.php create mode 100644 tests/predis/standalone/data/ar/Order.php create mode 100644 tests/predis/standalone/data/ar/OrderItem.php create mode 100644 tests/predis/standalone/data/ar/OrderItemWithNullFK.php create mode 100644 tests/predis/standalone/data/ar/OrderWithNullFK.php diff --git a/composer.json b/composer.json index 045be6205..597709e4b 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,13 @@ } ], "require": { + "php": ">=8.1", "yiisoft/yii2": "~2.0.39", - "ext-openssl": "*" + "ext-openssl": "*", + "predis/predis": "^3.0" }, "require-dev": { - "phpunit/phpunit": "<7", + "phpunit/phpunit": "9.*", "yiisoft/yii2-dev": "~2.0.39" }, "autoload": { diff --git a/src/Cache.php b/src/Cache.php index d9a4ad791..170f50448 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -139,7 +139,7 @@ class Cache extends \yii\caching\Cache * should be enabled, or false if it should be disabled. * @since 2.0.11 */ - public $forceClusterMode; + public $forceClusterMode = false; /** * @var bool whether redis [[Connection::$database|database]] is shared and can contain other data than cache. * Setting this to `true` will change [[flush()]] behavior - instead of using [`FLUSHDB`](https://redis.io/commands/flushdb) diff --git a/src/predis/Command/CommandDecorator.php b/src/predis/Command/CommandDecorator.php new file mode 100644 index 000000000..bbe21de83 --- /dev/null +++ b/src/predis/Command/CommandDecorator.php @@ -0,0 +1,35 @@ +originalCommand = $command; + } + + /** + * @inheritdoc + */ + public function parseResponse($data) + { + return $data; // Ваша реализация + } + + // Делегируем все остальные вызовы оригинальной команде + public function __call($method, $args) + { + return call_user_func_array([$this->originalCommand, $method], $args); + } + + public function getId():string { return $this->originalCommand->getId(); } + public function setArguments(array $arguments):void { $this->originalCommand->setArguments($arguments); } + public function getArguments():array { return $this->originalCommand->getArguments(); } +} diff --git a/src/predis/Command/HashGetAllCommand.php b/src/predis/Command/HashGetAllCommand.php new file mode 100644 index 000000000..7ae5e81d0 --- /dev/null +++ b/src/predis/Command/HashGetAllCommand.php @@ -0,0 +1,18 @@ +commands['HGETALL'] = HashGetAllCommand::class; +// $this->commands['ZRANGE'] = ZrangeCommand::class; + } + +} diff --git a/src/PredisConnection.php b/src/predis/PredisConnection.php similarity index 71% rename from src/PredisConnection.php rename to src/predis/PredisConnection.php index 5f4f6ace6..0f14977c1 100644 --- a/src/PredisConnection.php +++ b/src/predis/PredisConnection.php @@ -1,7 +1,7 @@ open(); @@ -175,7 +104,8 @@ public function executeCommand($name, $params = []) // $aaa = $this->database; // $this->clientSocket->select(1); - $res = $this->clientSocket->executeCommand($this->clientSocket->createCommand($name, $params)); + $command = $this->clientSocket->createCommand($name, $params); + $res = $this->clientSocket->executeCommand(new CommandDecorator($command)); return $res; } @@ -197,12 +127,12 @@ public function open(): void Yii::debug('Opening redis DB connection', __METHOD__); - $otp = array_merge([ - 'commands' => new YiiCommandFactory(), - $this->options, - ]); +// $otp = array_merge([ +// 'commands' => new CommandFactory(), +// $this->options, +// ]); - $this->clientSocket = new Client($this->parameters, $otp); + $this->clientSocket = new Client($this->parameters, $this->options); $this->initConnection(); } diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index b8aecf771..078f1808d 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -5,18 +5,57 @@ version: '3.8' services: PHP: - image: "yiisoftware/yii2-php:7.4-apache" + image: "yiisoftware/yii2-php:8.1-apache" networks: - yii2-redis volumes: - ../..:/app # Mount source-code for development - Redis: + redis: image: "redis" + environment: + - REDIS_REPLICATION_MODE=master networks: - yii2-redis ports: - - "6379:6379" + - "6399:6379" + + redis-sentinel-1: + image: bitnami/redis-sentinel:latest + environment: + - REDIS_MASTER_HOST=redis + - REDIS_MASTER_PORT_NUMBER=6379 + - REDIS_SENTINEL_QUORUM=1 + networks: + - yii2-redis + ports: + - '26379:26379' + depends_on: + - redis + redis-sentinel-2: + image: bitnami/redis-sentinel:latest + environment: + - REDIS_MASTER_HOST=redis + - REDIS_MASTER_PORT_NUMBER=6379 + - REDIS_SENTINEL_QUORUM=3 + networks: + - yii2-redis + ports: + - '26380:26379' + depends_on: + - redis + redis-sentinel-3: + image: bitnami/redis-sentinel:latest + environment: + - REDIS_MASTER_HOST=redis + - REDIS_MASTER_PORT_NUMBER=6379 + - REDIS_SENTINEL_QUORUM=3 + networks: + - yii2-redis + ports: + - '26381:26379' + depends_on: + - redis networks: yii2-redis: diff --git a/tests/predis/sentinel/ActiveDataProviderTest.php b/tests/predis/sentinel/ActiveDataProviderTest.php new file mode 100644 index 000000000..e5414a4a2 --- /dev/null +++ b/tests/predis/sentinel/ActiveDataProviderTest.php @@ -0,0 +1,37 @@ +getConnection(); + + $item = new Item(); + $item->setAttributes(['name' => 'abc', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'def', 'category_id' => 2], false); + $item->save(false); + } + + public function testQuery() + { + $query = Item::find(); + $provider = new ActiveDataProvider(['query' => $query]); + $this->assertCount(2, $provider->getModels()); + + $query = Item::find()->where(['category_id' => 1]); + $provider = new ActiveDataProvider(['query' => $query]); + $this->assertCount(1, $provider->getModels()); + } +} diff --git a/tests/predis/sentinel/ActiveRecordTest.php b/tests/predis/sentinel/ActiveRecordTest.php new file mode 100644 index 000000000..c0c9254da --- /dev/null +++ b/tests/predis/sentinel/ActiveRecordTest.php @@ -0,0 +1,689 @@ +getConnection(); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2, 'profile_id' => 2], false); + $customer->save(false); + +// INSERT INTO category (name) VALUES ('Books'); +// INSERT INTO category (name) VALUES ('Movies'); + + $item = new Item(); + $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + // insert a record with non-integer PK + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 'nostr', 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + + } + + /** + * overridden because null values are not part of the asArray result in redis + */ + public function testFindAsArray(): void + { + /* @var $customerClass \yii\db\ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + // asArray + $customer = $customerClass::find()->where(['id' => 2])->asArray()->one(); + $this->assertEquals([ + 'id' => 2, + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => 1, + ], $customer); + + // find all asArray + $customers = $customerClass::find()->asArray()->all(); + $this->assertCount(3, $customers); + $this->assertArrayHasKey('id', $customers[0]); + $this->assertArrayHasKey('name', $customers[0]); + $this->assertArrayHasKey('email', $customers[0]); + $this->assertArrayHasKey('address', $customers[0]); + $this->assertArrayHasKey('status', $customers[0]); + $this->assertArrayHasKey('id', $customers[1]); + $this->assertArrayHasKey('name', $customers[1]); + $this->assertArrayHasKey('email', $customers[1]); + $this->assertArrayHasKey('address', $customers[1]); + $this->assertArrayHasKey('status', $customers[1]); + $this->assertArrayHasKey('id', $customers[2]); + $this->assertArrayHasKey('name', $customers[2]); + $this->assertArrayHasKey('email', $customers[2]); + $this->assertArrayHasKey('address', $customers[2]); + $this->assertArrayHasKey('status', $customers[2]); + } + + public function testStatisticalFind(): void + { + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); + $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(2, Customer::find()->average('id')); + $this->assertEquals(1, Customer::find()->min('id')); + $this->assertEquals(3, Customer::find()->max('id')); + + $this->assertEquals(7, OrderItem::find()->count()); + $this->assertEquals(8, OrderItem::find()->sum('quantity')); + } + + // TODO test serial column incr + + public function testUpdatePk(): void + { + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + /** @var OrderItem $orderItem */ + $orderItem = OrderItem::findOne($pk); + $this->assertEquals(2, $orderItem->order_id); + $this->assertEquals(4, $orderItem->item_id); + + $orderItem->order_id = 2; + $orderItem->item_id = 10; + $orderItem->save(); + + $this->assertNull(OrderItem::findOne($pk)); + $this->assertNotNull(OrderItem::findOne(['order_id' => 2, 'item_id' => 10])); + } + + public function testFilterWhere(): void + { + // should work with hash format + $query = new ActiveQuery('dummy'); + $query->filterWhere([ + 'id' => 0, + 'title' => ' ', + 'author_ids' => [], + ]); + $this->assertEquals(['id' => 0], $query->where); + + $query->andFilterWhere(['status' => null]); + $this->assertEquals(['id' => 0], $query->where); + + $query->orFilterWhere(['name' => '']); + $this->assertEquals(['id' => 0], $query->where); + + // should work with operator format + $query = new ActiveQuery('dummy'); + $condition = ['like', 'name', 'Alex']; + $query->filterWhere($condition); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['between', 'id', null, null]); + $this->assertEquals($condition, $query->where); + + $query->orFilterWhere(['not between', 'id', null, null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['like', 'id', '']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['or like', 'id', '']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not like', 'id', ' ']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['or not like', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['>', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['>=', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['<', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['<=', 'id', null]); + $this->assertEquals($condition, $query->where); + } + + public function testFilterWhereRecursively(): void + { + $query = new ActiveQuery('dummy'); + $query->filterWhere(['and', ['like', 'name', ''], ['like', 'title', ''], ['id' => 1], ['not', ['like', 'name', '']]]); + $this->assertEquals(['and', ['id' => 1]], $query->where); + } + + public function testAutoIncrement(): void + { + Customer::getDb()->executeCommand('FLUSHDB'); + + $customer = new Customer(); + $customer->setAttributes(['id' => 4, 'email' => 'user4@example.com', 'name' => 'user4', 'address' => 'address4', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(4, $customer->id); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user5@example.com', 'name' => 'user5', 'address' => 'address5', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(5, $customer->id); + + $customer = new Customer(); + $customer->setAttributes(['id' => 1, 'email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(1, $customer->id); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user6@example.com', 'name' => 'user6', 'address' => 'address6', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(6, $customer->id); + + + /** @var Customer $customer */ + $customer = Customer::findOne(4); + $this->assertNotNull($customer); + $this->assertEquals('user4', $customer->name); + + $customer = Customer::findOne(5); + $this->assertNotNull($customer); + $this->assertEquals('user5', $customer->name); + + $customer = Customer::findOne(1); + $this->assertNotNull($customer); + $this->assertEquals('user1', $customer->name); + + $customer = Customer::findOne(6); + $this->assertNotNull($customer); + $this->assertEquals('user6', $customer->name); + } + + public function testEscapeData(): void + { + $customer = new Customer(); + $customer->email = "the People's Republic of China"; + $customer->save(false); + + /** @var Customer $c */ + $c = Customer::findOne(['email' => "the People's Republic of China"]); + $this->assertSame("the People's Republic of China", $c->email); + } + + public function testFindEmptyWith(): void + { + Order::getDb()->flushdb(); + $orders = Order::find() + ->where(['total' => 100000]) + ->orWhere(['total' => 1]) + ->with('customer') + ->all(); + $this->assertEquals([], $orders); + } + + public function testEmulateExecution(): void + { + $rows = Order::find() + ->emulateExecution() + ->all(); + $this->assertSame([], $rows); + + $row = Order::find() + ->emulateExecution() + ->one(); + $this->assertSame(null, $row); + + $exists = Order::find() + ->emulateExecution() + ->exists(); + $this->assertSame(false, $exists); + + $count = Order::find() + ->emulateExecution() + ->count(); + $this->assertSame(0, $count); + + $sum = Order::find() + ->emulateExecution() + ->sum('id'); + $this->assertSame(0, $sum); + + $sum = Order::find() + ->emulateExecution() + ->average('id'); + $this->assertSame(0, $sum); + + $max = Order::find() + ->emulateExecution() + ->max('id'); + $this->assertSame(null, $max); + + $min = Order::find() + ->emulateExecution() + ->min('id'); + $this->assertSame(null, $min); + + $scalar = Order::find() + ->emulateExecution() + ->scalar('id'); + $this->assertSame(null, $scalar); + + $column = Order::find() + ->emulateExecution() + ->column('id'); + $this->assertSame([], $column); + } + + /** + * @see https://github.com/yiisoft/yii2-redis/issues/93 + */ + public function testDeleteAllWithCondition(): void + { + $deletedCount = Order::deleteAll(['in', 'id', [1, 2, 3]]); + $this->assertEquals(3, $deletedCount); + } + + public function testBuildKey(): void + { + $pk = ['order_id' => 3, 'item_id' => 'nostr']; + $key = OrderItem::buildKey($pk); + + $orderItem = OrderItem::findOne($pk); + $this->assertNotNull($orderItem); + + $pk = ['order_id' => $orderItem->order_id, 'item_id' => $orderItem->item_id]; + $this->assertEquals($key, OrderItem::buildKey($pk)); + } + + public function testNotCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['not', ['customer_id' => 2]])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + } + + public function testBetweenCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['between', 'total', 30, 50])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['not between', 'total', 30, 50])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + } + + public function testInCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['in', 'customer_id', [1, 2]])->all(); + $this->assertCount(3, $orders); + + $orders = $orderClass::find()->where(['not in', 'customer_id', [1, 2]])->all(); + $this->assertCount(0, $orders); + + $orders = $orderClass::find()->where(['in', 'customer_id', [1]])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + + $orders = $orderClass::find()->where(['in', 'customer_id', [2]])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + } + + public function testCountQuery(): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $itemClass::find(); + $this->assertEquals(5, $query->count()); + + $query = $itemClass::find()->where(['category_id' => 1]); + $this->assertEquals(2, $query->count()); + + // negative values deactivate limit and offset (in case they were set before) + $query = $itemClass::find()->where(['category_id' => 1])->limit(-1)->offset(-1); + $this->assertEquals(2, $query->count()); + } + + public function illegalValuesForWhere(): array + { + return [ + [['id' => ["' .. redis.call('FLUSHALL') .. '" => 1]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL']], + [['id' => ['`id`=`id` and 1' => 1]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'legal' => 1, + '`id`=`id` and 1' => 1, + ]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'nested_illegal' => [ + 'false or 1=' => 1, + ], + ]], [], ['false or 1=']], + ]; + } + + /** + * @dataProvider illegalValuesForWhere + */ + public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $itemClass::find()->where($filterWithInjection['id']); + $lua = new LuaScriptBuilder(); + $script = $lua->buildOne($query); + + foreach ($expectedStrings as $string) { + $this->assertStringContainsString($string, $script); + } + foreach ($unexpectedStrings as $string) { + $this->assertStringNotContainsString($string, $script); + } + } + + public function illegalValuesForFindByCondition(): array + { + return [ + // code injection + [['id' => ["' .. redis.call('FLUSHALL') .. '" => 1]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL'], ["' .. redis.call('FLUSHALL') .. '"]], + [['id' => ['`id`=`id` and 1' => 1]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'legal' => 1, + '`id`=`id` and 1' => 1, + ]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'nested_illegal' => [ + 'false or 1=' => 1, + ], + ]], [], ['false or 1=']], + + // custom condition injection + [['id' => [ + 'or', + '1=1', + 'id' => 'id', + ]], ["cid0=='or' or cid0=='1=1' or cid0=='id'"], []], + [['id' => [ + 0 => 'or', + 'first' => '1=1', + 'second' => 1, + ]], ["cid0=='or' or cid0=='1=1' or cid0=='1'"], []], + [['id' => [ + 'name' => 'test', + 'email' => 'test@example.com', + "' .. redis.call('FLUSHALL') .. '" => "' .. redis.call('FLUSHALL') .. '", + ]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL'], ["' .. redis.call('FLUSHALL') .. '"]], + ]; + } + + /** + * @dataProvider illegalValuesForFindByCondition + */ + public function testValueEscapingInFindByCondition($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $this->invokeMethod(new $itemClass, 'findByCondition', [$filterWithInjection['id']]); + $lua = new LuaScriptBuilder(); + $script = $lua->buildOne($query); + + foreach ($expectedStrings as $string) { + $this->assertStringContainsString($string, $script); + } + foreach ($unexpectedStrings as $string) { + $this->assertStringNotContainsString($string, $script); + } + // ensure injected FLUSHALL call did not succeed + $query->one(); + $this->assertGreaterThan(3, $itemClass::find()->count()); + } + + public function testCompareCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['>', 'total', 30])->all(); + $this->assertCount(3, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + $this->assertEquals(2, $orders[2]['customer_id']); + + $orders = $orderClass::find()->where(['>=', 'total', 40])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['<', 'total', 41])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['<=', 'total', 40])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + } + + public function testStringCompareCondition(): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $items = $itemClass::find()->where(['>', 'name', 'A'])->all(); + $this->assertCount(5, $items); + $this->assertSame('Agile Web Application Development with Yii1.1 and PHP5', $items[0]['name']); + + $items = $itemClass::find()->where(['>=', 'name', 'Ice Age'])->all(); + $this->assertCount(3, $items); + $this->assertSame('Yii 1.1 Application Development Cookbook', $items[0]['name']); + $this->assertSame('Toy Story', $items[2]['name']); + + $items = $itemClass::find()->where(['<', 'name', 'Cars'])->all(); + $this->assertCount(1, $items); + $this->assertSame('Agile Web Application Development with Yii1.1 and PHP5', $items[0]['name']); + + $items = $itemClass::find()->where(['<=', 'name', 'Carts'])->all(); + $this->assertCount(2, $items); + } + + public function testFind(): void + { + /* @var $customerClass \yii\db\ActiveRecordInterface|string */ + $customerClass = $this->getCustomerClass(); + + // find one + /* @var $this TestCase|ActiveRecordTestTrait */ + $result = $customerClass::find(); + $this->assertInstanceOf('\\yii\\db\\ActiveQueryInterface', $result); + $customer = $result->one(); + $this->assertInstanceOf($customerClass, $customer); + + // find all + $customers = $customerClass::find()->all(); + $this->assertCount(3, $customers); + $this->assertInstanceOf($customerClass, $customers[0]); + $this->assertInstanceOf($customerClass, $customers[1]); + $this->assertInstanceOf($customerClass, $customers[2]); + + // find by a single primary key + $customer = $customerClass::findOne(2); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne(5); + $this->assertNull($customer); + $customer = $customerClass::findOne(['id' => [5, 6, 1]]); + $this->assertInstanceOf($customerClass, $customer); + $customer = $customerClass::find()->where(['id' => [5, 6, 1]])->one(); + $this->assertNotNull($customer); + + // find by column values + $customer = $customerClass::findOne(['id' => 2, 'name' => 'user2']); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne(['id' => 2, 'name' => 'user1']); + $this->assertNull($customer); + $customer = $customerClass::findOne(['id' => 5]); + $this->assertNull($customer); + $customer = $customerClass::findOne(['name' => 'user5']); + $this->assertNull($customer); + + // find by attributes + $customer = $customerClass::find()->where(['name' => 'user2'])->one(); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals(2, $customer->id); + + // scope + $this->assertCount(2, $customerClass::find()->active()->all()); + $this->assertEquals(2, $customerClass::find()->active()->count()); + } +} diff --git a/tests/predis/sentinel/RedisCacheTest.php b/tests/predis/sentinel/RedisCacheTest.php new file mode 100644 index 000000000..1d4caa969 --- /dev/null +++ b/tests/predis/sentinel/RedisCacheTest.php @@ -0,0 +1,223 @@ +markTestSkipped('No redis server connection configured.'); + } + $connection = new PredisConnection($params); +// if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { +// $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); +// } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new Cache(); + } + + return $this->_cacheInstance; + } + + protected function resetCacheInstance() + { + $this->getCacheInstance()->redis->flushdb(); + $this->_cacheInstance = null; + } + + public function testExpireMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_test_ms')); + } + + public function testExpireAddMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_testa_ms')); + } + + /** + * Store a value that is 2 times buffer size big + * https://github.com/yiisoft/yii2/issues/743 + */ + public function testLargeData() + { + $cache = $this->getCacheInstance(); + + $data = str_repeat('XX', 8192); // https://www.php.net/manual/en/function.fread.php + $key = 'bigdata1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + + // try with multibyte string + $data = str_repeat('ЖЫ', 8192); // https://www.php.net/manual/en/function.fread.php + $key = 'bigdata2'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + } + + /** + * Store a megabyte and see how it goes + * https://github.com/yiisoft/yii2/issues/6547 + */ + public function testReallyLargeData() + { + $cache = $this->getCacheInstance(); + + $keys = []; + for ($i = 1; $i < 16; $i++) { + $key = 'realbigdata' . $i; + $data = str_repeat('X', 100 * 1024); // 100 KB + $keys[$key] = $data; + +// $this->assertTrue($cache->get($key) === false); // do not display 100KB in terminal if this fails :) + $cache->set($key, $data); + } + $values = $cache->multiGet(array_keys($keys)); + foreach ($keys as $key => $value) { + $this->assertArrayHasKey($key, $values); + $this->assertSame($values[$key], $value); + } + } + + public function testMultiByteGetAndSet() + { + $cache = $this->getCacheInstance(); + + $data = ['abc' => 'ежик', 2 => 'def']; + $key = 'data1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + } + + public function testReplica() + { + $this->resetCacheInstance(); + + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $key = 'replica-1'; + $value = 'replica'; + + //No Replica listed + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + $databases = TestCase::getParam('databases'); + $redis = isset($databases['redis']) ? $databases['redis'] : null; + + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertSame($cache->get($key), $value); + + //One Replica listed + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + //Multiple Replicas listed + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + //invalid config + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $cache->replicas = ['redis']; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + $this->resetCacheInstance(); + } + + public function testFlushWithSharedDatabase() + { + $instance = $this->getCacheInstance(); + $instance->shareDatabase = true; + $instance->keyPrefix = 'myprefix_'; + $instance->redis->set('testkey', 'testvalue'); + + for ($i = 0; $i < 1000; $i++) { + $instance->set(sha1($i), uniqid('', true)); + } + $keys = $instance->redis->keys('*'); + $this->assertCount(1001, $keys); + + $instance->flush(); + + $keys = $instance->redis->keys('*'); + $this->assertCount(1, $keys); + $this->assertSame(['testkey'], $keys); + } +} diff --git a/tests/predis/sentinel/RedisConnectionTest.php b/tests/predis/sentinel/RedisConnectionTest.php new file mode 100644 index 000000000..78bafc0a1 --- /dev/null +++ b/tests/predis/sentinel/RedisConnectionTest.php @@ -0,0 +1,228 @@ +getConnection(false); + parent::tearDown(); + } + + /** + * test connection to redis and selection of db + */ + public function testConnect(): void + { + $db = $this->getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->set('YIITESTKEY', 'YIITESTVALUE'); + $db->close(); + + $db = $this->getConnection(false); + $db->open(); + $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); + $db->close(); + + $db = $this->getConnection(false); + $db->getClientSocket()->select(1); + $db->open(); + $this->assertNull($db->get('YIITESTKEY')); + $db->close(); + } + + /** + * tests whether close cleans up correctly so that a new connect works + */ + public function testReConnect(): void + { + $db = $this->getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->close(); + + $db->open(); + $this->assertTrue($db->ping()); + $db->close(); + } + + + /** + * @return array + */ + public function keyValueData(): array + { + return [ + [123], + [-123], + [0], + ['test'], + ["test\r\ntest"], + [''], + ]; + } + + /** + * @dataProvider keyValueData + * @param mixed $data + * @throws InvalidConfigException + */ + public function testStoreGet(mixed $data): void + { + $db = $this->getConnection(true); + + $db->set('hi', $data); + $this->assertEquals($data, $db->get('hi')); + } + + /** + * https://github.com/yiisoft/yii2/issues/4745 + */ + public function testReturnType(): void + { + $redis = $this->getConnection(); + $redis->executeCommand('SET', ['key1', 'val1']); + $redis->executeCommand('HMSET', ['hash1', 'hk3', 'hv3', 'hk4', 'hv4']); + $redis->executeCommand('RPUSH', ['newlist2', 'tgtgt', 'tgtt', '44', 11]); + $redis->executeCommand('SADD', ['newset2', 'segtggttval', 'sv1', 'sv2', 'sv3']); + $redis->executeCommand('ZADD', ['newz2', 2, 'ss', 3, 'pfpf']); + $allKeys = $redis->executeCommand('KEYS', ['*']); + sort($allKeys); + $this->assertEquals(['hash1', 'key1', 'newlist2', 'newset2', 'newz2'], $allKeys); + $expected = [ + 'hash1' => 'hash', + 'key1' => 'string', + 'newlist2' => 'list', + 'newset2' => 'set', + 'newz2' => 'zset', + ]; + foreach ($allKeys as $key) { + $this->assertEquals($expected[$key], $redis->executeCommand('TYPE', [$key])); + } + } + + + /** + * @return array + */ + public function zRangeByScoreData(): array + { + return [ + [ + 'members' => [ + ['foo', 1], + ['bar', 2], + ], + 'cases' => [ + // without both scores and limit + ['0', '(1', null, null, null, null, []], + ['1', '(2', null, null, null, null, ['foo']], + ['2', '(3', null, null, null, null, ['bar']], + ['(0', '2', null, null, null, null, ['foo', 'bar']], + + // with scores, but no limit + ['0', '(1', 'WITHSCORES', null, null, null, []], + ['1', '(2', 'WITHSCORES', null, null, null, ['foo', 1]], + ['2', '(3', 'WITHSCORES', null, null, null, ['bar', 2]], + ['(0', '2', 'WITHSCORES', null, null, null, ['foo', 1, 'bar', 2]], + + // with limit, but no scores + ['0', '(1', null, 'LIMIT', 0, 1, []], + ['1', '(2', null, 'LIMIT', 0, 1, ['foo']], + ['2', '(3', null, 'LIMIT', 0, 1, ['bar']], + ['(0', '2', null, 'LIMIT', 0, 1, ['foo']], + + // with both scores and limit + ['0', '(1', 'WITHSCORES', 'LIMIT', 0, 1, []], + ['1', '(2', 'WITHSCORES', 'LIMIT', 0, 1, ['foo', 1]], + ['2', '(3', 'WITHSCORES', 'LIMIT', 0, 1, ['bar', 2]], + ['(0', '2', 'WITHSCORES', 'LIMIT', 0, 1, ['foo', 1]], + ], + ], + ]; + } + + /** + * @dataProvider zRangeByScoreData + * @param array $members + * @param array $cases + * @throws InvalidConfigException + */ + public function testZRangeByScore(array $members, array $cases): void + { + $redis = $this->getConnection(); + $set = 'zrangebyscore'; + foreach ($members as $member) { + [$name, $score] = $member; + + $this->assertEquals(1, $redis->zadd($set, $score, $name)); + } + + foreach ($cases as $case) { + [$min, $max, $withScores, $limit, $offset, $count, $expectedRows] = $case; + if ($withScores !== null && $limit !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $withScores, $limit, $offset, $count); + } else if ($withScores !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $withScores); + } else if ($limit !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $limit, $offset, $count); + } else { + $rows = $redis->zrangebyscore($set, $min, $max); + } + $this->assertIsArray($rows); + $this->assertSameSize($expectedRows, $rows); + for ($i = 0, $iMax = count($expectedRows); $i < $iMax; $i++) { + $this->assertEquals($expectedRows[$i], $rows[$i]); + } + } + } + + /** + * @return array + */ + public function hmSetData(): array + { + return [ + [ + ['hmset1', 'one', '1', 'two', '2', 'three', '3'], + [ + 'one' => '1', + 'two' => '2', + 'three' => '3', + ], + ], + [ + ['hmset2', 'one', null, 'two', '2', 'three', '3'], + [ + 'one' => '', + 'two' => '2', + 'three' => '3', + ], + ], + ]; + } + + /** + * @dataProvider hmSetData + * @param array $params + * @param array $pairs + * @throws InvalidConfigException + */ + public function testHMSet(array $params, array $pairs): void + { + $redis = $this->getConnection(); + $set = $params[0]; + call_user_func_array([$redis, 'hmset'], $params); + foreach ($pairs as $field => $expected) { + $actual = $redis->hget($set, $field); + $this->assertEquals($expected, $actual); + } + } +} diff --git a/tests/predis/sentinel/RedisMutexTest.php b/tests/predis/sentinel/RedisMutexTest.php new file mode 100644 index 000000000..80cc98b30 --- /dev/null +++ b/tests/predis/sentinel/RedisMutexTest.php @@ -0,0 +1,149 @@ +createMutex(); + + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + + $this->assertTrue($mutex->acquire(static::$mutexName)); + $this->assertMutexKeyInRedis(); + $this->assertTrue($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + + // Double release + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + } + + public function testExpiration() + { + $mutex = $this->createMutex(); + + $this->assertTrue($mutex->acquire(static::$mutexName)); + $this->assertMutexKeyInRedis(); + $this->assertLessThanOrEqual(1500, $mutex->redis->executeCommand('PTTL', [$this->getKey(static::$mutexName)])); + + sleep(2); + + $this->assertMutexKeyNotInRedis(); + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + } + + public function acquireTimeoutProvider() + { + return [ + 'no timeout (lock is held)' => [0, false, false], + '2s (lock is held)' => [1, false, false], + '3s (lock will be auto released in acquire())' => [2, true, false], + '3s (lock is auto released)' => [2, true, true], + ]; + } + + /** + * @covers \yii\redis\Mutex::acquireLock + * @covers \yii\redis\Mutex::releaseLock + * @dataProvider acquireTimeoutProvider + */ + public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTimeout, $lockIsReleased) + { + $mutexOne = $this->createMutex(); + $mutexTwo = $this->createMutex(); + + $this->assertTrue($mutexOne->acquire(static::$mutexName)); + $this->assertFalse($mutexTwo->acquire(static::$mutexName)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + $this->assertTrue($mutexTwo->acquire(static::$mutexName)); + + if ($canAcquireAfterTimeout) { + // Mutex 2 auto released the lock or it will be auto released automatically + if ($lockIsReleased) { + sleep($timeout); + } + $this->assertSame($lockIsReleased, !$mutexTwo->release(static::$mutexName)); + + $this->assertTrue($mutexOne->acquire(static::$mutexName, $timeout)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + } else { + // Mutex 2 still holds the lock + $this->assertMutexKeyInRedis(); + + $this->assertFalse($mutexOne->acquire(static::$mutexName, $timeout)); + + $this->assertTrue($mutexTwo->release(static::$mutexName)); + $this->assertTrue($mutexOne->acquire(static::$mutexName)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + } + } + + protected function setUp(): void + { + parent::setUp(); + $databases = TestCase::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + + return; + } + + $connection = new PredisConnection($params); + $this->mockApplication(['components' => ['redis' => $connection]]); + } + + /** + * @return Mutex + * @throws \yii\base\InvalidConfigException + */ + protected function createMutex(): Mutex + { + return Yii::createObject([ + 'class' => Mutex::class, + 'expire' => 1.5, + 'keyPrefix' => static::$mutexPrefix + ]); + } + + protected function getKey($name) + { + if (!isset(self::$_keys[$name])) { + $mutex = $this->createMutex(); + $method = new \ReflectionMethod($mutex, 'calculateKey'); + $method->setAccessible(true); + self::$_keys[$name] = $method->invoke($mutex, $name); + } + + return self::$_keys[$name]; + } + + protected function assertMutexKeyInRedis() + { + $this->assertNotNull(Yii::$app->redis->executeCommand('GET', [$this->getKey(static::$mutexName)])); + } + + protected function assertMutexKeyNotInRedis() + { + $this->assertNull(Yii::$app->redis->executeCommand('GET', [$this->getKey(static::$mutexName)])); + } +} diff --git a/tests/predis/sentinel/RedisSessionTest.php b/tests/predis/sentinel/RedisSessionTest.php new file mode 100644 index 000000000..f3cba7ae2 --- /dev/null +++ b/tests/predis/sentinel/RedisSessionTest.php @@ -0,0 +1,85 @@ +writeSession('test', 'session data'); + $this->assertEquals('session data', $session->readSession('test')); + $session->destroySession('test'); + $this->assertEquals('', $session->readSession('test')); + } + + /** + * Test set name. Also check set name twice and after open + * @runInSeparateProcess + */ + public function testSetName() + { + $session = new Session(); + $session->setName('oldName'); + + $this->assertEquals('oldName', $session->getName()); + + $session->open(); + $session->setName('newName'); + + $this->assertEquals('newName', $session->getName()); + + $session->destroy(); + } + + /** + * @depends testReadWrite + * @runInSeparateProcess + */ + public function testStrictMode() + { + //non-strict-mode test + $nonStrictSession = new Session([ + 'useStrictMode' => false, + ]); + $nonStrictSession->close(); + $nonStrictSession->destroySession('non-existing-non-strict'); + $nonStrictSession->setId('non-existing-non-strict'); + $nonStrictSession->open(); + $this->assertEquals('non-existing-non-strict', $nonStrictSession->getId()); + $nonStrictSession->close(); + + //strict-mode test + $strictSession = new Session([ + 'useStrictMode' => true, + ]); + $strictSession->close(); + $strictSession->destroySession('non-existing-strict'); + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $id = $strictSession->getId(); + $this->assertNotEquals('non-existing-strict', $id); + $strictSession->set('strict_mode_test', 'session data'); + $strictSession->close(); + //Ensure session was not stored under forced id + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $this->assertNotEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + //Ensure session can be accessed with the new (and thus existing) id. + $strictSession->setId($id); + $strictSession->open(); + $this->assertNotEmpty($id); + $this->assertEquals($id, $strictSession->getId()); + $this->assertEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + } +} diff --git a/tests/predis/sentinel/TestCase.php b/tests/predis/sentinel/TestCase.php new file mode 100644 index 000000000..b7d3760b7 --- /dev/null +++ b/tests/predis/sentinel/TestCase.php @@ -0,0 +1,143 @@ +destroyApplication(); + } + + /** + * Populates Yii::$app with a new application + * The application will be destroyed on tearDown() automatically. + * + * @param array $config The application configuration, if needed + * @param string $appClass name of the application class to create + */ + protected function mockApplication(array $config = [], $appClass = '\yii\console\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + ], $config)); + } + + /** + * Mocks web application + * + * @param array $config + * @param string $appClass + */ + protected function mockWebApplication(array $config = [], $appClass = '\yii\web\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ], $config)); + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + Yii::$app = null; + Yii::$container = new Container(); + } + + protected function setUp(): void + { + $databases = self::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + $this->assertNotNull($params, 'No redis server connection configured.'); + + $this->mockApplication(['components' => ['redis' => new PredisConnection($params)]]); + + parent::setUp(); + } + + /** + * @param boolean $reset whether to clean up the test database + * @return PredisConnection + * @throws InvalidConfigException + */ + public function getConnection(bool $reset = true): PredisConnection + { + $databases = self::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : []; + $db = new PredisConnection($params); + if ($reset) { + $db->open(); + $db->flushdb(); + } + + return $db; + } + + /** + * Invokes a inaccessible method. + * + * @param $object + * @param $method + * @param array $args + * @param bool $revoke whether to make method inaccessible after execution + * @return mixed + */ + protected function invokeMethod($object, $method, $args = [], $revoke = true) + { + $reflection = new \ReflectionObject($object); + $method = $reflection->getMethod($method); + $method->setAccessible(true); + $result = $method->invokeArgs($object, $args); + if ($revoke) { + $method->setAccessible(false); + } + + return $result; + } +} diff --git a/tests/predis/sentinel/UniqueValidatorTest.php b/tests/predis/sentinel/UniqueValidatorTest.php new file mode 100644 index 000000000..8e9dfea74 --- /dev/null +++ b/tests/predis/sentinel/UniqueValidatorTest.php @@ -0,0 +1,155 @@ +getConnection(true); + + $validator = new UniqueValidator(); + + $customer = new \yiiunit\extensions\redis\data\ar\Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + + $this->assertFalse($customer->hasErrors('email')); + $validator->validateAttribute($customer, 'email'); + $this->assertFalse($customer->hasErrors('email')); + $customer->save(false); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + + $this->assertFalse($customer->hasErrors('email')); + $validator->validateAttribute($customer, 'email'); + $this->assertTrue($customer->hasErrors('email')); + } + + public function testValidationUpdate() + { + ActiveRecord::$db = $this->getConnection(true); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => 2], false); + $customer->save(false); + + $validator = new UniqueValidator(); + + $customer1 = Customer::findOne(['email' => 'user1@example.com']); + + $this->assertFalse($customer1->hasErrors('email')); + $validator->validateAttribute($customer1, 'email'); + $this->assertFalse($customer1->hasErrors('email')); + + $customer1->email = 'user2@example.com'; + $validator->validateAttribute($customer1, 'email'); + $this->assertTrue($customer1->hasErrors('email')); + } + + public function testValidationInsertCompositePk() + { + ActiveRecord::$db = $this->getConnection(true); + + $validator = new UniqueValidator(); + $validator->targetAttribute = ['order_id', 'item_id']; + + $model = new \yiiunit\extensions\redis\data\ar\OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('item_id')); + $validator->validateAttribute($model, 'item_id'); + $this->assertFalse($model->hasErrors('item_id')); + $model->save(false); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('item_id')); + $validator->validateAttribute($model, 'item_id'); + $this->assertTrue($model->hasErrors('item_id')); + } + + public function testValidationInsertCompositePkUniqueAttribute() + { + ActiveRecord::$db = $this->getConnection(true); + + $validator = new UniqueValidator(); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('quantity')); + $validator->validateAttribute($model, 'quantity'); + $this->assertFalse($model->hasErrors('quantity')); + $model->save(false); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('quantity')); + $validator->validateAttribute($model, 'quantity'); + $this->assertTrue($model->hasErrors('quantity')); + } + + public function testValidationUpdateCompositePk() + { + ActiveRecord::$db = $this->getConnection(true); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + + $validator = new UniqueValidator(); + $validator->targetAttribute = ['order_id', 'item_id']; + + $model1 = OrderItem::findOne(['order_id' => 1, 'item_id' => 1]); + + $this->assertFalse($model1->hasErrors('item_id')); + $validator->validateAttribute($model1, 'item_id'); + $this->assertFalse($model1->hasErrors('item_id')); + + $model1->item_id = 2; + $validator->validateAttribute($model1, 'item_id'); + $this->assertTrue($model1->hasErrors('item_id')); + } + + public function testValidationUpdateCompositePkUniqueAttribute() + { + ActiveRecord::$db = $this->getConnection(true); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 6, 'subtotal' => 42], false); + $model->save(false); + + $validator = new UniqueValidator(); + + $model1 = OrderItem::findOne(['order_id' => 1, 'item_id' => 1]); + + $this->assertFalse($model1->hasErrors('quantity')); + $validator->validateAttribute($model1, 'quantity'); + $this->assertFalse($model1->hasErrors('quantity')); + + $model1->quantity = 6; + $validator->validateAttribute($model1, 'quantity'); + $this->assertTrue($model1->hasErrors('quantity')); + } + +} diff --git a/tests/predis/sentinel/config/config.php b/tests/predis/sentinel/config/config.php new file mode 100644 index 000000000..37e4882ec --- /dev/null +++ b/tests/predis/sentinel/config/config.php @@ -0,0 +1,29 @@ + [ + 'redis' => [ +// 'class' => PredisConnection::class, + 'parameters' => ['tcp://redis-sentinel-1:26379', 'tcp://redis-sentinel-2:26379', 'tcp://redis-sentinel-3:26379'], + 'options' => [ + 'replication' => 'sentinel', + 'service' => 'mymaster', + 'parameters' => [ + 'password' => null, + 'database' => 0, + /** @see \Predis\Connection\StreamConnection */ + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ], +]; + +return $config; diff --git a/tests/predis/sentinel/data/ar/ActiveRecord.php b/tests/predis/sentinel/data/ar/ActiveRecord.php new file mode 100644 index 000000000..11df0e83f --- /dev/null +++ b/tests/predis/sentinel/data/ar/ActiveRecord.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class ActiveRecord extends \yii\redis\ActiveRecord +{ + /** + * @return PredisConnection + */ + public static $db; + + /** + * @return PredisConnection + */ + public static function getDb(): PredisConnection + { + return self::$db; + } +} diff --git a/tests/predis/sentinel/data/ar/Customer.php b/tests/predis/sentinel/data/ar/Customer.php new file mode 100644 index 000000000..481b837e3 --- /dev/null +++ b/tests/predis/sentinel/data/ar/Customer.php @@ -0,0 +1,105 @@ +hasMany(Order::className(), ['customer_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getExpensiveOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->andWhere("tonumber(redis.call('HGET','order' .. ':a:' .. pk, 'total')) > 50"); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getExpensiveOrdersWithNullFK() + { + return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id'])->andWhere("tonumber(redis.call('HGET','order' .. ':a:' .. pk, 'total')) > 50"); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrdersWithNullFK() + { + return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrdersWithItems() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->with('orderItems'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id'])->via('orders'); + } + + /** + * @inheritdoc + */ + public function afterSave($insert, $changedAttributes) + { + ActiveRecordTest::$afterSaveInsert = $insert; + ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; + parent::afterSave($insert, $changedAttributes); + } + + /** + * @inheritdoc + * @return CustomerQuery + */ + public static function find() + { + return new CustomerQuery(get_called_class()); + } +} diff --git a/tests/predis/sentinel/data/ar/CustomerQuery.php b/tests/predis/sentinel/data/ar/CustomerQuery.php new file mode 100644 index 000000000..c6b01bf79 --- /dev/null +++ b/tests/predis/sentinel/data/ar/CustomerQuery.php @@ -0,0 +1,21 @@ +andWhere(['status' => 1]); + + return $this; + } +} diff --git a/tests/predis/sentinel/data/ar/Item.php b/tests/predis/sentinel/data/ar/Item.php new file mode 100644 index 000000000..c4379a347 --- /dev/null +++ b/tests/predis/sentinel/data/ar/Item.php @@ -0,0 +1,21 @@ +hasOne(Customer::className(), ['id' => 'customer_id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + // additional query configuration + }); + } + + public function getExpensiveItemsUsingViaWithCallable() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function (\yii\redis\ActiveQuery $q) { + $q->where(['>=', 'subtotal', 10]); + }); + } + + public function getCheapItemsUsingViaWithCallable() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function (\yii\redis\ActiveQuery $q) { + $q->where(['<', 'subtotal', 10]); + }); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsIndexed() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems')->indexBy('id'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsWithNullFK() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItemsWithNullFK'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItemsWithNullFK() + { + return $this->hasMany(OrderItemWithNullFK::className(), ['order_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsInOrder1() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_ASC]); + })->orderBy('name'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsInOrder2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_DESC]); + })->orderBy('name'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getBooks() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems') + ->where(['category_id' => 1]); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getBooksWithNullFK() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItemsWithNullFK') + ->where(['category_id' => 1]); + } + + /** + * @inheritdoc + */ + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + $this->created_at = time(); + + return true; + } else { + return false; + } + } +} diff --git a/tests/predis/sentinel/data/ar/OrderItem.php b/tests/predis/sentinel/data/ar/OrderItem.php new file mode 100644 index 000000000..051f4671b --- /dev/null +++ b/tests/predis/sentinel/data/ar/OrderItem.php @@ -0,0 +1,53 @@ +hasOne(Order::className(), ['id' => 'order_id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItem() + { + return $this->hasOne(Item::className(), ['id' => 'item_id']); + } + + + + +} diff --git a/tests/predis/sentinel/data/ar/OrderItemWithNullFK.php b/tests/predis/sentinel/data/ar/OrderItemWithNullFK.php new file mode 100644 index 000000000..cb927467d --- /dev/null +++ b/tests/predis/sentinel/data/ar/OrderItemWithNullFK.php @@ -0,0 +1,30 @@ +getConnection(); + + $item = new Item(); + $item->setAttributes(['name' => 'abc', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'def', 'category_id' => 2], false); + $item->save(false); + } + + public function testQuery() + { + $query = Item::find(); + $provider = new ActiveDataProvider(['query' => $query]); + $this->assertCount(2, $provider->getModels()); + + $query = Item::find()->where(['category_id' => 1]); + $provider = new ActiveDataProvider(['query' => $query]); + $this->assertCount(1, $provider->getModels()); + } +} diff --git a/tests/predis/standalone/ActiveRecordTest.php b/tests/predis/standalone/ActiveRecordTest.php new file mode 100644 index 000000000..5070dd609 --- /dev/null +++ b/tests/predis/standalone/ActiveRecordTest.php @@ -0,0 +1,689 @@ +getConnection(); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2, 'profile_id' => 2], false); + $customer->save(false); + +// INSERT INTO category (name) VALUES ('Books'); +// INSERT INTO category (name) VALUES ('Movies'); + + $item = new Item(); + $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); + $item->save(false); + $item = new Item(); + $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + // insert a record with non-integer PK + $orderItem = new OrderItem(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 'nostr', 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new OrderWithNullFK(); + $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); + $order->save(false); + + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 8.0], false); + $orderItem->save(false); + $orderItem = new OrderItemWithNullFK(); + $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); + $orderItem->save(false); + + } + + /** + * overridden because null values are not part of the asArray result in redis + */ + public function testFindAsArray(): void + { + /* @var $customerClass \yii\db\ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + // asArray + $customer = $customerClass::find()->where(['id' => 2])->asArray()->one(); + $this->assertEquals([ + 'id' => 2, + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => 1, + ], $customer); + + // find all asArray + $customers = $customerClass::find()->asArray()->all(); + $this->assertCount(3, $customers); + $this->assertArrayHasKey('id', $customers[0]); + $this->assertArrayHasKey('name', $customers[0]); + $this->assertArrayHasKey('email', $customers[0]); + $this->assertArrayHasKey('address', $customers[0]); + $this->assertArrayHasKey('status', $customers[0]); + $this->assertArrayHasKey('id', $customers[1]); + $this->assertArrayHasKey('name', $customers[1]); + $this->assertArrayHasKey('email', $customers[1]); + $this->assertArrayHasKey('address', $customers[1]); + $this->assertArrayHasKey('status', $customers[1]); + $this->assertArrayHasKey('id', $customers[2]); + $this->assertArrayHasKey('name', $customers[2]); + $this->assertArrayHasKey('email', $customers[2]); + $this->assertArrayHasKey('address', $customers[2]); + $this->assertArrayHasKey('status', $customers[2]); + } + + public function testStatisticalFind(): void + { + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); + $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(2, Customer::find()->average('id')); + $this->assertEquals(1, Customer::find()->min('id')); + $this->assertEquals(3, Customer::find()->max('id')); + + $this->assertEquals(7, OrderItem::find()->count()); + $this->assertEquals(8, OrderItem::find()->sum('quantity')); + } + + // TODO test serial column incr + + public function testUpdatePk(): void + { + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + /** @var OrderItem $orderItem */ + $orderItem = OrderItem::findOne($pk); + $this->assertEquals(2, $orderItem->order_id); + $this->assertEquals(4, $orderItem->item_id); + + $orderItem->order_id = 2; + $orderItem->item_id = 10; + $orderItem->save(); + + $this->assertNull(OrderItem::findOne($pk)); + $this->assertNotNull(OrderItem::findOne(['order_id' => 2, 'item_id' => 10])); + } + + public function testFilterWhere(): void + { + // should work with hash format + $query = new ActiveQuery('dummy'); + $query->filterWhere([ + 'id' => 0, + 'title' => ' ', + 'author_ids' => [], + ]); + $this->assertEquals(['id' => 0], $query->where); + + $query->andFilterWhere(['status' => null]); + $this->assertEquals(['id' => 0], $query->where); + + $query->orFilterWhere(['name' => '']); + $this->assertEquals(['id' => 0], $query->where); + + // should work with operator format + $query = new ActiveQuery('dummy'); + $condition = ['like', 'name', 'Alex']; + $query->filterWhere($condition); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['between', 'id', null, null]); + $this->assertEquals($condition, $query->where); + + $query->orFilterWhere(['not between', 'id', null, null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not in', 'id', []]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['like', 'id', '']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['or like', 'id', '']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['not like', 'id', ' ']); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['or not like', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['>', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['>=', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['<', 'id', null]); + $this->assertEquals($condition, $query->where); + + $query->andFilterWhere(['<=', 'id', null]); + $this->assertEquals($condition, $query->where); + } + + public function testFilterWhereRecursively(): void + { + $query = new ActiveQuery('dummy'); + $query->filterWhere(['and', ['like', 'name', ''], ['like', 'title', ''], ['id' => 1], ['not', ['like', 'name', '']]]); + $this->assertEquals(['and', ['id' => 1]], $query->where); + } + + public function testAutoIncrement(): void + { + Customer::getDb()->executeCommand('FLUSHDB'); + + $customer = new Customer(); + $customer->setAttributes(['id' => 4, 'email' => 'user4@example.com', 'name' => 'user4', 'address' => 'address4', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(4, $customer->id); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user5@example.com', 'name' => 'user5', 'address' => 'address5', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(5, $customer->id); + + $customer = new Customer(); + $customer->setAttributes(['id' => 1, 'email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(1, $customer->id); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user6@example.com', 'name' => 'user6', 'address' => 'address6', 'status' => 1, 'profile_id' => null], false); + $customer->save(false); + $this->assertEquals(6, $customer->id); + + + /** @var Customer $customer */ + $customer = Customer::findOne(4); + $this->assertNotNull($customer); + $this->assertEquals('user4', $customer->name); + + $customer = Customer::findOne(5); + $this->assertNotNull($customer); + $this->assertEquals('user5', $customer->name); + + $customer = Customer::findOne(1); + $this->assertNotNull($customer); + $this->assertEquals('user1', $customer->name); + + $customer = Customer::findOne(6); + $this->assertNotNull($customer); + $this->assertEquals('user6', $customer->name); + } + + public function testEscapeData(): void + { + $customer = new Customer(); + $customer->email = "the People's Republic of China"; + $customer->save(false); + + /** @var Customer $c */ + $c = Customer::findOne(['email' => "the People's Republic of China"]); + $this->assertSame("the People's Republic of China", $c->email); + } + + public function testFindEmptyWith(): void + { + Order::getDb()->flushdb(); + $orders = Order::find() + ->where(['total' => 100000]) + ->orWhere(['total' => 1]) + ->with('customer') + ->all(); + $this->assertEquals([], $orders); + } + + public function testEmulateExecution(): void + { + $rows = Order::find() + ->emulateExecution() + ->all(); + $this->assertSame([], $rows); + + $row = Order::find() + ->emulateExecution() + ->one(); + $this->assertSame(null, $row); + + $exists = Order::find() + ->emulateExecution() + ->exists(); + $this->assertSame(false, $exists); + + $count = Order::find() + ->emulateExecution() + ->count(); + $this->assertSame(0, $count); + + $sum = Order::find() + ->emulateExecution() + ->sum('id'); + $this->assertSame(0, $sum); + + $sum = Order::find() + ->emulateExecution() + ->average('id'); + $this->assertSame(0, $sum); + + $max = Order::find() + ->emulateExecution() + ->max('id'); + $this->assertSame(null, $max); + + $min = Order::find() + ->emulateExecution() + ->min('id'); + $this->assertSame(null, $min); + + $scalar = Order::find() + ->emulateExecution() + ->scalar('id'); + $this->assertSame(null, $scalar); + + $column = Order::find() + ->emulateExecution() + ->column('id'); + $this->assertSame([], $column); + } + + /** + * @see https://github.com/yiisoft/yii2-redis/issues/93 + */ + public function testDeleteAllWithCondition(): void + { + $deletedCount = Order::deleteAll(['in', 'id', [1, 2, 3]]); + $this->assertEquals(3, $deletedCount); + } + + public function testBuildKey(): void + { + $pk = ['order_id' => 3, 'item_id' => 'nostr']; + $key = OrderItem::buildKey($pk); + + $orderItem = OrderItem::findOne($pk); + $this->assertNotNull($orderItem); + + $pk = ['order_id' => $orderItem->order_id, 'item_id' => $orderItem->item_id]; + $this->assertEquals($key, OrderItem::buildKey($pk)); + } + + public function testNotCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['not', ['customer_id' => 2]])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + } + + public function testBetweenCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['between', 'total', 30, 50])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['not between', 'total', 30, 50])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + } + + public function testInCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['in', 'customer_id', [1, 2]])->all(); + $this->assertCount(3, $orders); + + $orders = $orderClass::find()->where(['not in', 'customer_id', [1, 2]])->all(); + $this->assertCount(0, $orders); + + $orders = $orderClass::find()->where(['in', 'customer_id', [1]])->all(); + $this->assertCount(1, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + + $orders = $orderClass::find()->where(['in', 'customer_id', [2]])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + } + + public function testCountQuery(): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $itemClass::find(); + $this->assertEquals(5, $query->count()); + + $query = $itemClass::find()->where(['category_id' => 1]); + $this->assertEquals(2, $query->count()); + + // negative values deactivate limit and offset (in case they were set before) + $query = $itemClass::find()->where(['category_id' => 1])->limit(-1)->offset(-1); + $this->assertEquals(2, $query->count()); + } + + public function illegalValuesForWhere(): array + { + return [ + [['id' => ["' .. redis.call('FLUSHALL') .. '" => 1]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL']], + [['id' => ['`id`=`id` and 1' => 1]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'legal' => 1, + '`id`=`id` and 1' => 1, + ]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'nested_illegal' => [ + 'false or 1=' => 1, + ], + ]], [], ['false or 1=']], + ]; + } + + /** + * @dataProvider illegalValuesForWhere + */ + public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $itemClass::find()->where($filterWithInjection['id']); + $lua = new LuaScriptBuilder(); + $script = $lua->buildOne($query); + + foreach ($expectedStrings as $string) { + $this->assertStringContainsString($string, $script); + } + foreach ($unexpectedStrings as $string) { + $this->assertStringNotContainsString($string, $script); + } + } + + public function illegalValuesForFindByCondition(): array + { + return [ + // code injection + [['id' => ["' .. redis.call('FLUSHALL') .. '" => 1]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL'], ["' .. redis.call('FLUSHALL') .. '"]], + [['id' => ['`id`=`id` and 1' => 1]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'legal' => 1, + '`id`=`id` and 1' => 1, + ]], ["'`id`=`id` and 1'", 'ididand']], + [['id' => [ + 'nested_illegal' => [ + 'false or 1=' => 1, + ], + ]], [], ['false or 1=']], + + // custom condition injection + [['id' => [ + 'or', + '1=1', + 'id' => 'id', + ]], ["cid0=='or' or cid0=='1=1' or cid0=='id'"], []], + [['id' => [ + 0 => 'or', + 'first' => '1=1', + 'second' => 1, + ]], ["cid0=='or' or cid0=='1=1' or cid0=='1'"], []], + [['id' => [ + 'name' => 'test', + 'email' => 'test@example.com', + "' .. redis.call('FLUSHALL') .. '" => "' .. redis.call('FLUSHALL') .. '", + ]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL'], ["' .. redis.call('FLUSHALL') .. '"]], + ]; + } + + /** + * @dataProvider illegalValuesForFindByCondition + */ + public function testValueEscapingInFindByCondition($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + $query = $this->invokeMethod(new $itemClass, 'findByCondition', [$filterWithInjection['id']]); + $lua = new LuaScriptBuilder(); + $script = $lua->buildOne($query); + + foreach ($expectedStrings as $string) { + $this->assertStringContainsString($string, $script); + } + foreach ($unexpectedStrings as $string) { + $this->assertStringNotContainsString($string, $script); + } + // ensure injected FLUSHALL call did not succeed + $query->one(); + $this->assertGreaterThan(3, $itemClass::find()->count()); + } + + public function testCompareCondition(): void + { + /* @var $orderClass \yii\db\ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->where(['>', 'total', 30])->all(); + $this->assertCount(3, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + $this->assertEquals(2, $orders[2]['customer_id']); + + $orders = $orderClass::find()->where(['>=', 'total', 40])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(1, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['<', 'total', 41])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + + $orders = $orderClass::find()->where(['<=', 'total', 40])->all(); + $this->assertCount(2, $orders); + $this->assertEquals(2, $orders[0]['customer_id']); + $this->assertEquals(2, $orders[1]['customer_id']); + } + + public function testStringCompareCondition(): void + { + /* @var $itemClass \yii\db\ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $items = $itemClass::find()->where(['>', 'name', 'A'])->all(); + $this->assertCount(5, $items); + $this->assertSame('Agile Web Application Development with Yii1.1 and PHP5', $items[0]['name']); + + $items = $itemClass::find()->where(['>=', 'name', 'Ice Age'])->all(); + $this->assertCount(3, $items); + $this->assertSame('Yii 1.1 Application Development Cookbook', $items[0]['name']); + $this->assertSame('Toy Story', $items[2]['name']); + + $items = $itemClass::find()->where(['<', 'name', 'Cars'])->all(); + $this->assertCount(1, $items); + $this->assertSame('Agile Web Application Development with Yii1.1 and PHP5', $items[0]['name']); + + $items = $itemClass::find()->where(['<=', 'name', 'Carts'])->all(); + $this->assertCount(2, $items); + } + + public function testFind(): void + { + /* @var $customerClass \yii\db\ActiveRecordInterface|string */ + $customerClass = $this->getCustomerClass(); + + // find one + /* @var $this TestCase|ActiveRecordTestTrait */ + $result = $customerClass::find(); + $this->assertInstanceOf('\\yii\\db\\ActiveQueryInterface', $result); + $customer = $result->one(); + $this->assertInstanceOf($customerClass, $customer); + + // find all + $customers = $customerClass::find()->all(); + $this->assertCount(3, $customers); + $this->assertInstanceOf($customerClass, $customers[0]); + $this->assertInstanceOf($customerClass, $customers[1]); + $this->assertInstanceOf($customerClass, $customers[2]); + + // find by a single primary key + $customer = $customerClass::findOne(2); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne(5); + $this->assertNull($customer); + $customer = $customerClass::findOne(['id' => [5, 6, 1]]); + $this->assertInstanceOf($customerClass, $customer); + $customer = $customerClass::find()->where(['id' => [5, 6, 1]])->one(); + $this->assertNotNull($customer); + + // find by column values + $customer = $customerClass::findOne(['id' => 2, 'name' => 'user2']); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne(['id' => 2, 'name' => 'user1']); + $this->assertNull($customer); + $customer = $customerClass::findOne(['id' => 5]); + $this->assertNull($customer); + $customer = $customerClass::findOne(['name' => 'user5']); + $this->assertNull($customer); + + // find by attributes + $customer = $customerClass::find()->where(['name' => 'user2'])->one(); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals(2, $customer->id); + + // scope + $this->assertCount(2, $customerClass::find()->active()->all()); + $this->assertEquals(2, $customerClass::find()->active()->count()); + } +} diff --git a/tests/predis/standalone/RedisCacheTest.php b/tests/predis/standalone/RedisCacheTest.php new file mode 100644 index 000000000..666164c5d --- /dev/null +++ b/tests/predis/standalone/RedisCacheTest.php @@ -0,0 +1,222 @@ +markTestSkipped('No redis server connection configured.'); + } + $connection = new PredisConnection($params); +// if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { +// $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); +// } + + $this->mockApplication(['components' => ['redis' => $connection]]); + + if ($this->_cacheInstance === null) { + $this->_cacheInstance = new Cache(); + } + + return $this->_cacheInstance; + } + + protected function resetCacheInstance() + { + $this->getCacheInstance()->redis->flushdb(); + $this->_cacheInstance = null; + } + + public function testExpireMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_test_ms')); + } + + public function testExpireAddMilliseconds() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2)); + usleep(100000); + $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms')); + usleep(300000); + $this->assertFalse($cache->get('expire_testa_ms')); + } + + /** + * Store a value that is 2 times buffer size big + * https://github.com/yiisoft/yii2/issues/743 + */ + public function testLargeData() + { + $cache = $this->getCacheInstance(); + + $data = str_repeat('XX', 8192); // https://www.php.net/manual/en/function.fread.php + $key = 'bigdata1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + + // try with multibyte string + $data = str_repeat('ЖЫ', 8192); // https://www.php.net/manual/en/function.fread.php + $key = 'bigdata2'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + } + + /** + * Store a megabyte and see how it goes + * https://github.com/yiisoft/yii2/issues/6547 + */ + public function testReallyLargeData() + { + $cache = $this->getCacheInstance(); + + $keys = []; + for ($i = 1; $i < 16; $i++) { + $key = 'realbigdata' . $i; + $data = str_repeat('X', 100 * 1024); // 100 KB + $keys[$key] = $data; + +// $this->assertTrue($cache->get($key) === false); // do not display 100KB in terminal if this fails :) + $cache->set($key, $data); + } + $values = $cache->multiGet(array_keys($keys)); + foreach ($keys as $key => $value) { + $this->assertArrayHasKey($key, $values); + $this->assertSame($values[$key], $value); + } + } + + public function testMultiByteGetAndSet() + { + $cache = $this->getCacheInstance(); + + $data = ['abc' => 'ежик', 2 => 'def']; + $key = 'data1'; + + $this->assertFalse($cache->get($key)); + $cache->set($key, $data); + $this->assertSame($cache->get($key), $data); + } + + public function testReplica() + { + $this->resetCacheInstance(); + + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $key = 'replica-1'; + $value = 'replica'; + + //No Replica listed + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + $databases = TestCase::getParam('databases'); + $redis = isset($databases['redis']) ? $databases['redis'] : null; + + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertSame($cache->get($key), $value); + + //One Replica listed + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + //Multiple Replicas listed + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $cache->replicas = [ + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + [ + 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', + 'password' => isset($redis['password']) ? $redis['password'] : null, + ], + ]; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + //invalid config + $this->resetCacheInstance(); + $cache = $this->getCacheInstance(); + $cache->enableReplicas = true; + + $cache->replicas = ['redis']; + $this->assertFalse($cache->get($key)); + $cache->set($key, $value); + $this->assertSame($cache->get($key), $value); + + $this->resetCacheInstance(); + } + + public function testFlushWithSharedDatabase() + { + $instance = $this->getCacheInstance(); + $instance->shareDatabase = true; + $instance->keyPrefix = 'myprefix_'; + $instance->redis->set('testkey', 'testvalue'); + + for ($i = 0; $i < 1000; $i++) { + $instance->set(sha1($i), uniqid('', true)); + } + $keys = $instance->redis->keys('*'); + $this->assertCount(1001, $keys); + + $instance->flush(); + + $keys = $instance->redis->keys('*'); + $this->assertCount(1, $keys); + $this->assertSame(['testkey'], $keys); + } +} diff --git a/tests/predis/standalone/RedisConnectionTest.php b/tests/predis/standalone/RedisConnectionTest.php new file mode 100644 index 000000000..a8435e12f --- /dev/null +++ b/tests/predis/standalone/RedisConnectionTest.php @@ -0,0 +1,228 @@ +getConnection(false); + parent::tearDown(); + } + + /** + * test connection to redis and selection of db + */ + public function testConnect(): void + { + $db = $this->getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->set('YIITESTKEY', 'YIITESTVALUE'); + $db->close(); + + $db = $this->getConnection(false); + $db->open(); + $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); + $db->close(); + + $db = $this->getConnection(false); + $db->getClientSocket()->select(1); + $db->open(); + $this->assertNull($db->get('YIITESTKEY')); + $db->close(); + } + + /** + * tests whether close cleans up correctly so that a new connect works + */ + public function testReConnect(): void + { + $db = $this->getConnection(false); + $db->open(); + $this->assertTrue($db->ping()); + $db->close(); + + $db->open(); + $this->assertTrue($db->ping()); + $db->close(); + } + + + /** + * @return array + */ + public function keyValueData(): array + { + return [ + [123], + [-123], + [0], + ['test'], + ["test\r\ntest"], + [''], + ]; + } + + /** + * @dataProvider keyValueData + * @param mixed $data + * @throws InvalidConfigException + */ + public function testStoreGet(mixed $data): void + { + $db = $this->getConnection(true); + + $db->set('hi', $data); + $this->assertEquals($data, $db->get('hi')); + } + + /** + * https://github.com/yiisoft/yii2/issues/4745 + */ + public function testReturnType(): void + { + $redis = $this->getConnection(); + $redis->executeCommand('SET', ['key1', 'val1']); + $redis->executeCommand('HMSET', ['hash1', 'hk3', 'hv3', 'hk4', 'hv4']); + $redis->executeCommand('RPUSH', ['newlist2', 'tgtgt', 'tgtt', '44', 11]); + $redis->executeCommand('SADD', ['newset2', 'segtggttval', 'sv1', 'sv2', 'sv3']); + $redis->executeCommand('ZADD', ['newz2', 2, 'ss', 3, 'pfpf']); + $allKeys = $redis->executeCommand('KEYS', ['*']); + sort($allKeys); + $this->assertEquals(['hash1', 'key1', 'newlist2', 'newset2', 'newz2'], $allKeys); + $expected = [ + 'hash1' => 'hash', + 'key1' => 'string', + 'newlist2' => 'list', + 'newset2' => 'set', + 'newz2' => 'zset', + ]; + foreach ($allKeys as $key) { + $this->assertEquals($expected[$key], $redis->executeCommand('TYPE', [$key])); + } + } + + + /** + * @return array + */ + public function zRangeByScoreData(): array + { + return [ + [ + 'members' => [ + ['foo', 1], + ['bar', 2], + ], + 'cases' => [ + // without both scores and limit + ['0', '(1', null, null, null, null, []], + ['1', '(2', null, null, null, null, ['foo']], + ['2', '(3', null, null, null, null, ['bar']], + ['(0', '2', null, null, null, null, ['foo', 'bar']], + + // with scores, but no limit + ['0', '(1', 'WITHSCORES', null, null, null, []], + ['1', '(2', 'WITHSCORES', null, null, null, ['foo', 1]], + ['2', '(3', 'WITHSCORES', null, null, null, ['bar', 2]], + ['(0', '2', 'WITHSCORES', null, null, null, ['foo', 1, 'bar', 2]], + + // with limit, but no scores + ['0', '(1', null, 'LIMIT', 0, 1, []], + ['1', '(2', null, 'LIMIT', 0, 1, ['foo']], + ['2', '(3', null, 'LIMIT', 0, 1, ['bar']], + ['(0', '2', null, 'LIMIT', 0, 1, ['foo']], + + // with both scores and limit + ['0', '(1', 'WITHSCORES', 'LIMIT', 0, 1, []], + ['1', '(2', 'WITHSCORES', 'LIMIT', 0, 1, ['foo', 1]], + ['2', '(3', 'WITHSCORES', 'LIMIT', 0, 1, ['bar', 2]], + ['(0', '2', 'WITHSCORES', 'LIMIT', 0, 1, ['foo', 1]], + ], + ], + ]; + } + + /** + * @dataProvider zRangeByScoreData + * @param array $members + * @param array $cases + * @throws InvalidConfigException + */ + public function testZRangeByScore(array $members, array $cases): void + { + $redis = $this->getConnection(); + $set = 'zrangebyscore'; + foreach ($members as $member) { + [$name, $score] = $member; + + $this->assertEquals(1, $redis->zadd($set, $score, $name)); + } + + foreach ($cases as $case) { + [$min, $max, $withScores, $limit, $offset, $count, $expectedRows] = $case; + if ($withScores !== null && $limit !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $withScores, $limit, $offset, $count); + } else if ($withScores !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $withScores); + } else if ($limit !== null) { + $rows = $redis->zrangebyscore($set, $min, $max, $limit, $offset, $count); + } else { + $rows = $redis->zrangebyscore($set, $min, $max); + } + $this->assertIsArray($rows); + $this->assertSameSize($expectedRows, $rows); + for ($i = 0, $iMax = count($expectedRows); $i < $iMax; $i++) { + $this->assertEquals($expectedRows[$i], $rows[$i]); + } + } + } + + /** + * @return array + */ + public function hmSetData(): array + { + return [ + [ + ['hmset1', 'one', '1', 'two', '2', 'three', '3'], + [ + 'one' => '1', + 'two' => '2', + 'three' => '3', + ], + ], + [ + ['hmset2', 'one', null, 'two', '2', 'three', '3'], + [ + 'one' => '', + 'two' => '2', + 'three' => '3', + ], + ], + ]; + } + + /** + * @dataProvider hmSetData + * @param array $params + * @param array $pairs + * @throws InvalidConfigException + */ + public function testHMSet(array $params, array $pairs): void + { + $redis = $this->getConnection(); + $set = $params[0]; + call_user_func_array([$redis, 'hmset'], $params); + foreach ($pairs as $field => $expected) { + $actual = $redis->hget($set, $field); + $this->assertEquals($expected, $actual); + } + } +} diff --git a/tests/predis/standalone/RedisMutexTest.php b/tests/predis/standalone/RedisMutexTest.php new file mode 100644 index 000000000..0363ff854 --- /dev/null +++ b/tests/predis/standalone/RedisMutexTest.php @@ -0,0 +1,149 @@ +createMutex(); + + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + + $this->assertTrue($mutex->acquire(static::$mutexName)); + $this->assertMutexKeyInRedis(); + $this->assertTrue($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + + // Double release + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + } + + public function testExpiration() + { + $mutex = $this->createMutex(); + + $this->assertTrue($mutex->acquire(static::$mutexName)); + $this->assertMutexKeyInRedis(); + $this->assertLessThanOrEqual(1500, $mutex->redis->executeCommand('PTTL', [$this->getKey(static::$mutexName)])); + + sleep(2); + + $this->assertMutexKeyNotInRedis(); + $this->assertFalse($mutex->release(static::$mutexName)); + $this->assertMutexKeyNotInRedis(); + } + + public function acquireTimeoutProvider() + { + return [ + 'no timeout (lock is held)' => [0, false, false], + '2s (lock is held)' => [1, false, false], + '3s (lock will be auto released in acquire())' => [2, true, false], + '3s (lock is auto released)' => [2, true, true], + ]; + } + + /** + * @covers \yii\redis\Mutex::acquireLock + * @covers \yii\redis\Mutex::releaseLock + * @dataProvider acquireTimeoutProvider + */ + public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTimeout, $lockIsReleased) + { + $mutexOne = $this->createMutex(); + $mutexTwo = $this->createMutex(); + + $this->assertTrue($mutexOne->acquire(static::$mutexName)); + $this->assertFalse($mutexTwo->acquire(static::$mutexName)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + $this->assertTrue($mutexTwo->acquire(static::$mutexName)); + + if ($canAcquireAfterTimeout) { + // Mutex 2 auto released the lock or it will be auto released automatically + if ($lockIsReleased) { + sleep($timeout); + } + $this->assertSame($lockIsReleased, !$mutexTwo->release(static::$mutexName)); + + $this->assertTrue($mutexOne->acquire(static::$mutexName, $timeout)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + } else { + // Mutex 2 still holds the lock + $this->assertMutexKeyInRedis(); + + $this->assertFalse($mutexOne->acquire(static::$mutexName, $timeout)); + + $this->assertTrue($mutexTwo->release(static::$mutexName)); + $this->assertTrue($mutexOne->acquire(static::$mutexName)); + $this->assertTrue($mutexOne->release(static::$mutexName)); + } + } + + protected function setUp(): void + { + parent::setUp(); + $databases = TestCase::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + if ($params === null) { + $this->markTestSkipped('No redis server connection configured.'); + + return; + } + + $connection = new PredisConnection($params); + $this->mockApplication(['components' => ['redis' => $connection]]); + } + + /** + * @return Mutex + * @throws \yii\base\InvalidConfigException + */ + protected function createMutex(): Mutex + { + return Yii::createObject([ + 'class' => Mutex::class, + 'expire' => 1.5, + 'keyPrefix' => static::$mutexPrefix + ]); + } + + protected function getKey($name) + { + if (!isset(self::$_keys[$name])) { + $mutex = $this->createMutex(); + $method = new \ReflectionMethod($mutex, 'calculateKey'); + $method->setAccessible(true); + self::$_keys[$name] = $method->invoke($mutex, $name); + } + + return self::$_keys[$name]; + } + + protected function assertMutexKeyInRedis() + { + $this->assertNotNull(Yii::$app->redis->executeCommand('GET', [$this->getKey(static::$mutexName)])); + } + + protected function assertMutexKeyNotInRedis() + { + $this->assertNull(Yii::$app->redis->executeCommand('GET', [$this->getKey(static::$mutexName)])); + } +} diff --git a/tests/predis/standalone/RedisSessionTest.php b/tests/predis/standalone/RedisSessionTest.php new file mode 100644 index 000000000..0e2192aae --- /dev/null +++ b/tests/predis/standalone/RedisSessionTest.php @@ -0,0 +1,85 @@ +writeSession('test', 'session data'); + $this->assertEquals('session data', $session->readSession('test')); + $session->destroySession('test'); + $this->assertEquals('', $session->readSession('test')); + } + + /** + * Test set name. Also check set name twice and after open + * @runInSeparateProcess + */ + public function testSetName() + { + $session = new Session(); + $session->setName('oldName'); + + $this->assertEquals('oldName', $session->getName()); + + $session->open(); + $session->setName('newName'); + + $this->assertEquals('newName', $session->getName()); + + $session->destroy(); + } + + /** + * @depends testReadWrite + * @runInSeparateProcess + */ + public function testStrictMode() + { + //non-strict-mode test + $nonStrictSession = new Session([ + 'useStrictMode' => false, + ]); + $nonStrictSession->close(); + $nonStrictSession->destroySession('non-existing-non-strict'); + $nonStrictSession->setId('non-existing-non-strict'); + $nonStrictSession->open(); + $this->assertEquals('non-existing-non-strict', $nonStrictSession->getId()); + $nonStrictSession->close(); + + //strict-mode test + $strictSession = new Session([ + 'useStrictMode' => true, + ]); + $strictSession->close(); + $strictSession->destroySession('non-existing-strict'); + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $id = $strictSession->getId(); + $this->assertNotEquals('non-existing-strict', $id); + $strictSession->set('strict_mode_test', 'session data'); + $strictSession->close(); + //Ensure session was not stored under forced id + $strictSession->setId('non-existing-strict'); + $strictSession->open(); + $this->assertNotEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + //Ensure session can be accessed with the new (and thus existing) id. + $strictSession->setId($id); + $strictSession->open(); + $this->assertNotEmpty($id); + $this->assertEquals($id, $strictSession->getId()); + $this->assertEquals('session data', $strictSession->get('strict_mode_test')); + $strictSession->close(); + } +} diff --git a/tests/predis/standalone/TestCase.php b/tests/predis/standalone/TestCase.php new file mode 100644 index 000000000..94d8dcfe1 --- /dev/null +++ b/tests/predis/standalone/TestCase.php @@ -0,0 +1,143 @@ +destroyApplication(); + } + + /** + * Populates Yii::$app with a new application + * The application will be destroyed on tearDown() automatically. + * + * @param array $config The application configuration, if needed + * @param string $appClass name of the application class to create + */ + protected function mockApplication(array $config = [], $appClass = '\yii\console\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + ], $config)); + } + + /** + * Mocks web application + * + * @param array $config + * @param string $appClass + */ + protected function mockWebApplication(array $config = [], $appClass = '\yii\web\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ], $config)); + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + Yii::$app = null; + Yii::$container = new Container(); + } + + protected function setUp(): void + { + $databases = self::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : null; + $this->assertNotNull($params, 'No redis server connection configured.'); + + $this->mockApplication(['components' => ['redis' => new PredisConnection($params)]]); + + parent::setUp(); + } + + /** + * @param boolean $reset whether to clean up the test database + * @return PredisConnection + * @throws InvalidConfigException + */ + public function getConnection(bool $reset = true): PredisConnection + { + $databases = self::getParam('databases'); + $params = isset($databases['redis']) ? $databases['redis'] : []; + $db = new PredisConnection($params); + if ($reset) { + $db->open(); + $db->flushdb(); + } + + return $db; + } + + /** + * Invokes a inaccessible method. + * + * @param $object + * @param $method + * @param array $args + * @param bool $revoke whether to make method inaccessible after execution + * @return mixed + */ + protected function invokeMethod($object, $method, $args = [], $revoke = true) + { + $reflection = new \ReflectionObject($object); + $method = $reflection->getMethod($method); + $method->setAccessible(true); + $result = $method->invokeArgs($object, $args); + if ($revoke) { + $method->setAccessible(false); + } + + return $result; + } +} diff --git a/tests/predis/standalone/UniqueValidatorTest.php b/tests/predis/standalone/UniqueValidatorTest.php new file mode 100644 index 000000000..ef856af20 --- /dev/null +++ b/tests/predis/standalone/UniqueValidatorTest.php @@ -0,0 +1,155 @@ +getConnection(true); + + $validator = new UniqueValidator(); + + $customer = new \yiiunit\extensions\redis\data\ar\Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + + $this->assertFalse($customer->hasErrors('email')); + $validator->validateAttribute($customer, 'email'); + $this->assertFalse($customer->hasErrors('email')); + $customer->save(false); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + + $this->assertFalse($customer->hasErrors('email')); + $validator->validateAttribute($customer, 'email'); + $this->assertTrue($customer->hasErrors('email')); + } + + public function testValidationUpdate() + { + ActiveRecord::$db = $this->getConnection(true); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1, 'profile_id' => 1], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1, 'profile_id' => 2], false); + $customer->save(false); + + $validator = new UniqueValidator(); + + $customer1 = Customer::findOne(['email' => 'user1@example.com']); + + $this->assertFalse($customer1->hasErrors('email')); + $validator->validateAttribute($customer1, 'email'); + $this->assertFalse($customer1->hasErrors('email')); + + $customer1->email = 'user2@example.com'; + $validator->validateAttribute($customer1, 'email'); + $this->assertTrue($customer1->hasErrors('email')); + } + + public function testValidationInsertCompositePk() + { + ActiveRecord::$db = $this->getConnection(true); + + $validator = new UniqueValidator(); + $validator->targetAttribute = ['order_id', 'item_id']; + + $model = new \yiiunit\extensions\redis\data\ar\OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('item_id')); + $validator->validateAttribute($model, 'item_id'); + $this->assertFalse($model->hasErrors('item_id')); + $model->save(false); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('item_id')); + $validator->validateAttribute($model, 'item_id'); + $this->assertTrue($model->hasErrors('item_id')); + } + + public function testValidationInsertCompositePkUniqueAttribute() + { + ActiveRecord::$db = $this->getConnection(true); + + $validator = new UniqueValidator(); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('quantity')); + $validator->validateAttribute($model, 'quantity'); + $this->assertFalse($model->hasErrors('quantity')); + $model->save(false); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + + $this->assertFalse($model->hasErrors('quantity')); + $validator->validateAttribute($model, 'quantity'); + $this->assertTrue($model->hasErrors('quantity')); + } + + public function testValidationUpdateCompositePk() + { + ActiveRecord::$db = $this->getConnection(true); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + + $validator = new UniqueValidator(); + $validator->targetAttribute = ['order_id', 'item_id']; + + $model1 = OrderItem::findOne(['order_id' => 1, 'item_id' => 1]); + + $this->assertFalse($model1->hasErrors('item_id')); + $validator->validateAttribute($model1, 'item_id'); + $this->assertFalse($model1->hasErrors('item_id')); + + $model1->item_id = 2; + $validator->validateAttribute($model1, 'item_id'); + $this->assertTrue($model1->hasErrors('item_id')); + } + + public function testValidationUpdateCompositePkUniqueAttribute() + { + ActiveRecord::$db = $this->getConnection(true); + + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 5, 'subtotal' => 42], false); + $model->save(false); + $model = new OrderItem(); + $model->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 6, 'subtotal' => 42], false); + $model->save(false); + + $validator = new UniqueValidator(); + + $model1 = OrderItem::findOne(['order_id' => 1, 'item_id' => 1]); + + $this->assertFalse($model1->hasErrors('quantity')); + $validator->validateAttribute($model1, 'quantity'); + $this->assertFalse($model1->hasErrors('quantity')); + + $model1->quantity = 6; + $validator->validateAttribute($model1, 'quantity'); + $this->assertTrue($model1->hasErrors('quantity')); + } + +} diff --git a/tests/predis/standalone/config/config.php b/tests/predis/standalone/config/config.php new file mode 100644 index 000000000..9fc417b91 --- /dev/null +++ b/tests/predis/standalone/config/config.php @@ -0,0 +1,27 @@ + [ + 'redis' => [ +// 'class' => PredisConnection::class, + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => null, + 'database' => 0, + /** @see \Predis\Connection\StreamConnection */ + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ], +]; + +return $config; diff --git a/tests/predis/standalone/data/ar/ActiveRecord.php b/tests/predis/standalone/data/ar/ActiveRecord.php new file mode 100644 index 000000000..274127ac9 --- /dev/null +++ b/tests/predis/standalone/data/ar/ActiveRecord.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class ActiveRecord extends \yii\redis\ActiveRecord +{ + /** + * @return PredisConnection + */ + public static $db; + + /** + * @return PredisConnection + */ + public static function getDb(): PredisConnection + { + return self::$db; + } +} diff --git a/tests/predis/standalone/data/ar/Customer.php b/tests/predis/standalone/data/ar/Customer.php new file mode 100644 index 000000000..6b3796e2a --- /dev/null +++ b/tests/predis/standalone/data/ar/Customer.php @@ -0,0 +1,105 @@ +hasMany(Order::className(), ['customer_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getExpensiveOrders() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->andWhere("tonumber(redis.call('HGET','order' .. ':a:' .. pk, 'total')) > 50"); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getExpensiveOrdersWithNullFK() + { + return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id'])->andWhere("tonumber(redis.call('HGET','order' .. ':a:' .. pk, 'total')) > 50"); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrdersWithNullFK() + { + return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrdersWithItems() + { + return $this->hasMany(Order::className(), ['customer_id' => 'id'])->with('orderItems'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id'])->via('orders'); + } + + /** + * @inheritdoc + */ + public function afterSave($insert, $changedAttributes) + { + ActiveRecordTest::$afterSaveInsert = $insert; + ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; + parent::afterSave($insert, $changedAttributes); + } + + /** + * @inheritdoc + * @return CustomerQuery + */ + public static function find() + { + return new CustomerQuery(get_called_class()); + } +} diff --git a/tests/predis/standalone/data/ar/CustomerQuery.php b/tests/predis/standalone/data/ar/CustomerQuery.php new file mode 100644 index 000000000..1cb2a77a5 --- /dev/null +++ b/tests/predis/standalone/data/ar/CustomerQuery.php @@ -0,0 +1,21 @@ +andWhere(['status' => 1]); + + return $this; + } +} diff --git a/tests/predis/standalone/data/ar/Item.php b/tests/predis/standalone/data/ar/Item.php new file mode 100644 index 000000000..18f84ee8c --- /dev/null +++ b/tests/predis/standalone/data/ar/Item.php @@ -0,0 +1,21 @@ +hasOne(Customer::className(), ['id' => 'customer_id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItems() + { + return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItems() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + // additional query configuration + }); + } + + public function getExpensiveItemsUsingViaWithCallable() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function (\yii\redis\ActiveQuery $q) { + $q->where(['>=', 'subtotal', 10]); + }); + } + + public function getCheapItemsUsingViaWithCallable() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function (\yii\redis\ActiveQuery $q) { + $q->where(['<', 'subtotal', 10]); + }); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsIndexed() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems')->indexBy('id'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsWithNullFK() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItemsWithNullFK'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getOrderItemsWithNullFK() + { + return $this->hasMany(OrderItemWithNullFK::className(), ['order_id' => 'id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsInOrder1() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_ASC]); + })->orderBy('name'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItemsInOrder2() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems', function ($q) { + $q->orderBy(['subtotal' => SORT_DESC]); + })->orderBy('name'); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getBooks() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItems') + ->where(['category_id' => 1]); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getBooksWithNullFK() + { + return $this->hasMany(Item::className(), ['id' => 'item_id']) + ->via('orderItemsWithNullFK') + ->where(['category_id' => 1]); + } + + /** + * @inheritdoc + */ + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + $this->created_at = time(); + + return true; + } else { + return false; + } + } +} diff --git a/tests/predis/standalone/data/ar/OrderItem.php b/tests/predis/standalone/data/ar/OrderItem.php new file mode 100644 index 000000000..b065f854b --- /dev/null +++ b/tests/predis/standalone/data/ar/OrderItem.php @@ -0,0 +1,53 @@ +hasOne(Order::className(), ['id' => 'order_id']); + } + + /** + * @return \yii\redis\ActiveQuery + */ + public function getItem() + { + return $this->hasOne(Item::className(), ['id' => 'item_id']); + } + + + + +} diff --git a/tests/predis/standalone/data/ar/OrderItemWithNullFK.php b/tests/predis/standalone/data/ar/OrderItemWithNullFK.php new file mode 100644 index 000000000..2cbd690f3 --- /dev/null +++ b/tests/predis/standalone/data/ar/OrderItemWithNullFK.php @@ -0,0 +1,30 @@ + Date: Tue, 1 Jul 2025 19:24:11 +0300 Subject: [PATCH 07/49] ConnectionInterface --- src/Cache.php | 4 +- src/Connection.php | 224 +--------------------------- src/ConnectionInterface.php | 229 +++++++++++++++++++++++++++++ src/Mutex.php | 4 +- src/Session.php | 4 +- src/predis/PredisConnection.php | 250 +++++++++++++++++++++++++++++++- 6 files changed, 486 insertions(+), 229 deletions(-) create mode 100644 src/ConnectionInterface.php diff --git a/src/Cache.php b/src/Cache.php index 170f50448..f55c003c8 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -391,7 +391,9 @@ protected function getReplica() $replicas = $this->replicas; shuffle($replicas); $config = array_shift($replicas); - $this->_replica = Instance::ensure($config, Connection::className()); + $class = $config['class'] ?? 'yii\redis\Connection'; + $connection = Yii::createObject($class, $config); + $this->_replica = Instance::ensure($connection, ConnectionInterface::class); return $this->_replica; } } diff --git a/src/Connection.php b/src/Connection.php index 3d35fd200..e6c5d7397 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -24,220 +24,6 @@ * When the server needs authentication, you can set the [[password]] property to * authenticate with the server after connect. * - * The execution of [redis commands](https://redis.io/commands) is possible with via [[executeCommand()]]. - * - * @method mixed append($key, $value) Append a value to a key. - * @method mixed auth($password) Authenticate to the server. - * @method mixed bgrewriteaof() Asynchronously rewrite the append-only file. - * @method mixed bgsave() Asynchronously save the dataset to disk. - * @method mixed bitcount($key, $start = null, $end = null) Count set bits in a string. - * @method mixed bitfield($key, ...$operations) Perform arbitrary bitfield integer operations on strings. - * @method mixed bitop($operation, $destkey, ...$keys) Perform bitwise operations between strings. - * @method mixed bitpos($key, $bit, $start = null, $end = null) Find first bit set or clear in a string. - * @method mixed blpop(...$keys, $timeout) Remove and get the first element in a list, or block until one is available. - * @method mixed brpop(...$keys, $timeout) Remove and get the last element in a list, or block until one is available. - * @method mixed brpoplpush($source, $destination, $timeout) Pop a value from a list, push it to another list and return it; or block until one is available. - * @method mixed clientKill(...$filters) Kill the connection of a client. - * @method mixed clientList() Get the list of client connections. - * @method mixed clientGetname() Get the current connection name. - * @method mixed clientPause($timeout) Stop processing commands from clients for some time. - * @method mixed clientReply($option) Instruct the server whether to reply to commands. - * @method mixed clientSetname($connectionName) Set the current connection name. - * @method mixed clusterAddslots(...$slots) Assign new hash slots to receiving node. - * @method mixed clusterCountkeysinslot($slot) Return the number of local keys in the specified hash slot. - * @method mixed clusterDelslots(...$slots) Set hash slots as unbound in receiving node. - * @method mixed clusterFailover($option = null) Forces a slave to perform a manual failover of its master.. - * @method mixed clusterForget($nodeId) Remove a node from the nodes table. - * @method mixed clusterGetkeysinslot($slot, $count) Return local key names in the specified hash slot. - * @method mixed clusterInfo() Provides info about Redis Cluster node state. - * @method mixed clusterKeyslot($key) Returns the hash slot of the specified key. - * @method mixed clusterMeet($ip, $port) Force a node cluster to handshake with another node. - * @method mixed clusterNodes() Get Cluster config for the node. - * @method mixed clusterReplicate($nodeId) Reconfigure a node as a slave of the specified master node. - * @method mixed clusterReset($resetType = "SOFT") Reset a Redis Cluster node. - * @method mixed clusterSaveconfig() Forces the node to save cluster state on disk. - * @method mixed clusterSetslot($slot, $type, $nodeid = null) Bind a hash slot to a specific node. - * @method mixed clusterSlaves($nodeId) List slave nodes of the specified master node. - * @method mixed clusterSlots() Get array of Cluster slot to node mappings. - * @method mixed command() Get array of Redis command details. - * @method mixed commandCount() Get total number of Redis commands. - * @method mixed commandGetkeys() Extract keys given a full Redis command. - * @method mixed commandInfo(...$commandNames) Get array of specific Redis command details. - * @method mixed configGet($parameter) Get the value of a configuration parameter. - * @method mixed configRewrite() Rewrite the configuration file with the in memory configuration. - * @method mixed configSet($parameter, $value) Set a configuration parameter to the given value. - * @method mixed configResetstat() Reset the stats returned by INFO. - * @method mixed dbsize() Return the number of keys in the selected database. - * @method mixed debugObject($key) Get debugging information about a key. - * @method mixed debugSegfault() Make the server crash. - * @method mixed decr($key) Decrement the integer value of a key by one. - * @method mixed decrby($key, $decrement) Decrement the integer value of a key by the given number. - * @method mixed del(...$keys) Delete a key. - * @method mixed discard() Discard all commands issued after MULTI. - * @method mixed dump($key) Return a serialized version of the value stored at the specified key.. - * @method mixed echo($message) Echo the given string. - * @method mixed eval($script, $numkeys, ...$keys, ...$args) Execute a Lua script server side. - * @method mixed evalsha($sha1, $numkeys, ...$keys, ...$args) Execute a Lua script server side. - * @method mixed exec() Execute all commands issued after MULTI. - * @method mixed exists(...$keys) Determine if a key exists. - * @method mixed expire($key, $seconds) Set a key's time to live in seconds. - * @method mixed expireat($key, $timestamp) Set the expiration for a key as a UNIX timestamp. - * @method mixed flushall($ASYNC = null) Remove all keys from all databases. - * @method mixed flushdb($ASYNC = null) Remove all keys from the current database. - * @method mixed geoadd($key, $longitude, $latitude, $member, ...$more) Add one or more geospatial items in the geospatial index represented using a sorted set. - * @method mixed geohash($key, ...$members) Returns members of a geospatial index as standard geohash strings. - * @method mixed geopos($key, ...$members) Returns longitude and latitude of members of a geospatial index. - * @method mixed geodist($key, $member1, $member2, $unit = null) Returns the distance between two members of a geospatial index. - * @method mixed georadius($key, $longitude, $latitude, $radius, $metric, ...$options) Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point. - * @method mixed georadiusbymember($key, $member, $radius, $metric, ...$options) Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member. - * @method mixed get($key) Get the value of a key. - * @method mixed getbit($key, $offset) Returns the bit value at offset in the string value stored at key. - * @method mixed getrange($key, $start, $end) Get a substring of the string stored at a key. - * @method mixed getset($key, $value) Set the string value of a key and return its old value. - * @method mixed hdel($key, ...$fields) Delete one or more hash fields. - * @method mixed hexists($key, $field) Determine if a hash field exists. - * @method mixed hget($key, $field) Get the value of a hash field. - * @method mixed hgetall($key) Get all the fields and values in a hash. - * @method mixed hincrby($key, $field, $increment) Increment the integer value of a hash field by the given number. - * @method mixed hincrbyfloat($key, $field, $increment) Increment the float value of a hash field by the given amount. - * @method mixed hkeys($key) Get all the fields in a hash. - * @method mixed hlen($key) Get the number of fields in a hash. - * @method mixed hmget($key, ...$fields) Get the values of all the given hash fields. - * @method mixed hmset($key, $field, $value, ...$more) Set multiple hash fields to multiple values. - * @method mixed hset($key, $field, $value) Set the string value of a hash field. - * @method mixed hsetnx($key, $field, $value) Set the value of a hash field, only if the field does not exist. - * @method mixed hstrlen($key, $field) Get the length of the value of a hash field. - * @method mixed hvals($key) Get all the values in a hash. - * @method mixed incr($key) Increment the integer value of a key by one. - * @method mixed incrby($key, $increment) Increment the integer value of a key by the given amount. - * @method mixed incrbyfloat($key, $increment) Increment the float value of a key by the given amount. - * @method mixed info($section = null) Get information and statistics about the server. - * @method mixed keys($pattern) Find all keys matching the given pattern. - * @method mixed lastsave() Get the UNIX time stamp of the last successful save to disk. - * @method mixed lindex($key, $index) Get an element from a list by its index. - * @method mixed linsert($key, $where, $pivot, $value) Insert an element before or after another element in a list. - * @method mixed llen($key) Get the length of a list. - * @method mixed lpop($key) Remove and get the first element in a list. - * @method mixed lpush($key, ...$values) Prepend one or multiple values to a list. - * @method mixed lpushx($key, $value) Prepend a value to a list, only if the list exists. - * @method mixed lrange($key, $start, $stop) Get a range of elements from a list. - * @method mixed lrem($key, $count, $value) Remove elements from a list. - * @method mixed lset($key, $index, $value) Set the value of an element in a list by its index. - * @method mixed ltrim($key, $start, $stop) Trim a list to the specified range. - * @method mixed mget(...$keys) Get the values of all the given keys. - * @method mixed migrate($host, $port, $key, $destinationDb, $timeout, ...$options) Atomically transfer a key from a Redis instance to another one.. - * @method mixed monitor() Listen for all requests received by the server in real time. - * @method mixed move($key, $db) Move a key to another database. - * @method mixed mset(...$keyValuePairs) Set multiple keys to multiple values. - * @method mixed msetnx(...$keyValuePairs) Set multiple keys to multiple values, only if none of the keys exist. - * @method mixed multi() Mark the start of a transaction block. - * @method mixed object($subcommand, ...$argumentss) Inspect the internals of Redis objects. - * @method mixed persist($key) Remove the expiration from a key. - * @method mixed pexpire($key, $milliseconds) Set a key's time to live in milliseconds. - * @method mixed pexpireat($key, $millisecondsTimestamp) Set the expiration for a key as a UNIX timestamp specified in milliseconds. - * @method mixed pfadd($key, ...$elements) Adds the specified elements to the specified HyperLogLog.. - * @method mixed pfcount(...$keys) Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).. - * @method mixed pfmerge($destkey, ...$sourcekeys) Merge N different HyperLogLogs into a single one.. - * @method mixed ping($message = null) Ping the server. - * @method mixed psetex($key, $milliseconds, $value) Set the value and expiration in milliseconds of a key. - * @method mixed psubscribe(...$patterns) Listen for messages published to channels matching the given patterns. - * @method mixed pubsub($subcommand, ...$arguments) Inspect the state of the Pub/Sub subsystem. - * @method mixed pttl($key) Get the time to live for a key in milliseconds. - * @method mixed publish($channel, $message) Post a message to a channel. - * @method mixed punsubscribe(...$patterns) Stop listening for messages posted to channels matching the given patterns. - * @method mixed quit() Close the connection. - * @method mixed randomkey() Return a random key from the keyspace. - * @method mixed readonly() Enables read queries for a connection to a cluster slave node. - * @method mixed readwrite() Disables read queries for a connection to a cluster slave node. - * @method mixed rename($key, $newkey) Rename a key. - * @method mixed renamenx($key, $newkey) Rename a key, only if the new key does not exist. - * @method mixed restore($key, $ttl, $serializedValue, $REPLACE = null) Create a key using the provided serialized value, previously obtained using DUMP.. - * @method mixed role() Return the role of the instance in the context of replication. - * @method mixed rpop($key) Remove and get the last element in a list. - * @method mixed rpoplpush($source, $destination) Remove the last element in a list, prepend it to another list and return it. - * @method mixed rpush($key, ...$values) Append one or multiple values to a list. - * @method mixed rpushx($key, $value) Append a value to a list, only if the list exists. - * @method mixed sadd($key, ...$members) Add one or more members to a set. - * @method mixed save() Synchronously save the dataset to disk. - * @method mixed scard($key) Get the number of members in a set. - * @method mixed scriptDebug($option) Set the debug mode for executed scripts.. - * @method mixed scriptExists(...$sha1s) Check existence of scripts in the script cache.. - * @method mixed scriptFlush() Remove all the scripts from the script cache.. - * @method mixed scriptKill() Kill the script currently in execution.. - * @method mixed scriptLoad($script) Load the specified Lua script into the script cache.. - * @method mixed sdiff(...$keys) Subtract multiple sets. - * @method mixed sdiffstore($destination, ...$keys) Subtract multiple sets and store the resulting set in a key. - * @method mixed select($index) Change the selected database for the current connection. - * @method mixed set($key, $value, ...$options) Set the string value of a key. - * @method mixed setbit($key, $offset, $value) Sets or clears the bit at offset in the string value stored at key. - * @method mixed setex($key, $seconds, $value) Set the value and expiration of a key. - * @method mixed setnx($key, $value) Set the value of a key, only if the key does not exist. - * @method mixed setrange($key, $offset, $value) Overwrite part of a string at key starting at the specified offset. - * @method mixed shutdown($saveOption = null) Synchronously save the dataset to disk and then shut down the server. - * @method mixed sinter(...$keys) Intersect multiple sets. - * @method mixed sinterstore($destination, ...$keys) Intersect multiple sets and store the resulting set in a key. - * @method mixed sismember($key, $member) Determine if a given value is a member of a set. - * @method mixed slaveof($host, $port) Make the server a slave of another instance, or promote it as master. - * @method mixed slowlog($subcommand, $argument = null) Manages the Redis slow queries log. - * @method mixed smembers($key) Get all the members in a set. - * @method mixed smove($source, $destination, $member) Move a member from one set to another. - * @method mixed sort($key, ...$options) Sort the elements in a list, set or sorted set. - * @method mixed spop($key, $count = null) Remove and return one or multiple random members from a set. - * @method mixed srandmember($key, $count = null) Get one or multiple random members from a set. - * @method mixed srem($key, ...$members) Remove one or more members from a set. - * @method mixed strlen($key) Get the length of the value stored in a key. - * @method mixed subscribe(...$channels) Listen for messages published to the given channels. - * @method mixed sunion(...$keys) Add multiple sets. - * @method mixed sunionstore($destination, ...$keys) Add multiple sets and store the resulting set in a key. - * @method mixed swapdb($index, $index) Swaps two Redis databases. - * @method mixed sync() Internal command used for replication. - * @method mixed time() Return the current server time. - * @method mixed touch(...$keys) Alters the last access time of a key(s). Returns the number of existing keys specified.. - * @method mixed ttl($key) Get the time to live for a key. - * @method mixed type($key) Determine the type stored at key. - * @method mixed unsubscribe(...$channels) Stop listening for messages posted to the given channels. - * @method mixed unlink(...$keys) Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.. - * @method mixed unwatch() Forget about all watched keys. - * @method mixed wait($numslaves, $timeout) Wait for the synchronous replication of all the write commands sent in the context of the current connection. - * @method mixed watch(...$keys) Watch the given keys to determine execution of the MULTI/EXEC block. - * @method mixed xack($stream, $group, ...$ids) Removes one or multiple messages from the pending entries list (PEL) of a stream consumer group - * @method mixed xadd($stream, $id, $field, $value, ...$fieldsValues) Appends the specified stream entry to the stream at the specified key - * @method mixed xclaim($stream, $group, $consumer, $minIdleTimeMs, $id, ...$options) Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument - * @method mixed xdel($stream, ...$ids) Removes the specified entries from a stream, and returns the number of entries deleted - * @method mixed xgroup($subCommand, $stream, $group, ...$options) Manages the consumer groups associated with a stream data structure - * @method mixed xinfo($subCommand, $stream, ...$options) Retrieves different information about the streams and associated consumer groups - * @method mixed xlen($stream) Returns the number of entries inside a stream - * @method mixed xpending($stream, $group, ...$options) Fetching data from a stream via a consumer group, and not acknowledging such data, has the effect of creating pending entries - * @method mixed xrange($stream, $start, $end, ...$options) Returns the stream entries matching a given range of IDs - * @method mixed xread(...$options) Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller - * @method mixed xreadgroup($subCommand, $group, $consumer, ...$options) Special version of the XREAD command with support for consumer groups - * @method mixed xrevrange($stream, $end, $start, ...$options) Exactly like XRANGE, but with the notable difference of returning the entries in reverse order, and also taking the start-end range in reverse order - * @method mixed xtrim($stream, $strategy, ...$options) Trims the stream to a given number of items, evicting older items (items with lower IDs) if needed - * @method mixed zadd($key, ...$options) Add one or more members to a sorted set, or update its score if it already exists. - * @method mixed zcard($key) Get the number of members in a sorted set. - * @method mixed zcount($key, $min, $max) Count the members in a sorted set with scores within the given values. - * @method mixed zincrby($key, $increment, $member) Increment the score of a member in a sorted set. - * @method mixed zinterstore($destination, $numkeys, $key, ...$options) Intersect multiple sorted sets and store the resulting sorted set in a new key. - * @method mixed zlexcount($key, $min, $max) Count the number of members in a sorted set between a given lexicographical range. - * @method mixed zrange($key, $start, $stop, $WITHSCORES = null) Return a range of members in a sorted set, by index. - * @method mixed zrangebylex($key, $min, $max, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by lexicographical range. - * @method mixed zrevrangebylex($key, $max, $min, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.. - * @method mixed zrangebyscore($key, $min, $max, ...$options) Return a range of members in a sorted set, by score. - * @method mixed zrank($key, $member) Determine the index of a member in a sorted set. - * @method mixed zrem($key, ...$members) Remove one or more members from a sorted set. - * @method mixed zremrangebylex($key, $min, $max) Remove all members in a sorted set between the given lexicographical range. - * @method mixed zremrangebyrank($key, $start, $stop) Remove all members in a sorted set within the given indexes. - * @method mixed zremrangebyscore($key, $min, $max) Remove all members in a sorted set within the given scores. - * @method mixed zrevrange($key, $start, $stop, $WITHSCORES = null) Return a range of members in a sorted set, by index, with scores ordered from high to low. - * @method mixed zrevrangebyscore($key, $max, $min, $WITHSCORES = null, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by score, with scores ordered from high to low. - * @method mixed zrevrank($key, $member) Determine the index of a member in a sorted set, with scores ordered from high to low. - * @method mixed zscore($key, $member) Get the score associated with the given member in a sorted set. - * @method mixed zunionstore($destination, $numkeys, $key, ...$options) Add multiple sorted sets and store the resulting sorted set in a new key. - * @method mixed scan($cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate the keys space. - * @method mixed sscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate Set elements. - * @method mixed hscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate hash fields and associated values. - * @method mixed zscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate sorted sets elements and associated scores. - * * @property-read string $connectionString Socket connection string. * @property-read string $driverName Name of the DB driver. * @property-read bool $isActive Whether the DB connection is established. @@ -247,7 +33,7 @@ * @author Carsten Brandt * @since 2.0 */ -class Connection extends Component +class Connection extends Component implements ConnectionInterface { /** * @event Event an event that is triggered after a DB connection is established @@ -614,7 +400,7 @@ public function getSocket() * Returns a value indicating whether the DB connection is established. * @return bool whether the DB connection is established */ - public function getIsActive() + public function getIsActive(): bool { return ArrayHelper::getValue($this->_pool, $this->connectionString, false) !== false; } @@ -624,7 +410,7 @@ public function getIsActive() * It does nothing if a DB connection has already been established. * @throws Exception if connection fails */ - public function open() + public function open(): void { if ($this->socket !== false) { return; @@ -668,7 +454,7 @@ public function open() * Closes the currently active DB connection. * It does nothing if the connection is already closed. */ - public function close() + public function close(): void { foreach ($this->_pool as $socket) { $connection = $this->connectionString . ', database=' . $this->database; @@ -759,7 +545,7 @@ public function __call($name, $params) * for details on the mentioned reply types. * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply). */ - public function executeCommand($name, $params = []) + public function executeCommand($name, $params = []): mixed { $this->open(); diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php new file mode 100644 index 000000000..31fa4b905 --- /dev/null +++ b/src/ConnectionInterface.php @@ -0,0 +1,229 @@ + + * @method mixed auth($password) Authenticate to the server. + * @method mixed bgrewriteaof() Asynchronously rewrite the append-only file. + * @method mixed bgsave() Asynchronously save the dataset to disk. + * @method mixed bitcount($key, $start = null, $end = null) Count set bits in a string. + * @method mixed bitfield($key, ...$operations) Perform arbitrary bitfield integer operations on strings. + * @method mixed bitop($operation, $destkey, ...$keys) Perform bitwise operations between strings. + * @method mixed bitpos($key, $bit, $start = null, $end = null) Find first bit set or clear in a string. + * @method mixed blpop(...$keys, $timeout) Remove and get the first element in a list, or block until one is available. + * @method mixed brpop(...$keys, $timeout) Remove and get the last element in a list, or block until one is available. + * @method mixed brpoplpush($source, $destination, $timeout) Pop a value from a list, push it to another list and return it; or block until one is available. + * @method mixed clientKill(...$filters) Kill the connection of a client. + * @method mixed clientList() Get the list of client connections. + * @method mixed clientGetname() Get the current connection name. + * @method mixed clientPause($timeout) Stop processing commands from clients for some time. + * @method mixed clientReply($option) Instruct the server whether to reply to commands. + * @method mixed clientSetname($connectionName) Set the current connection name. + * @method mixed clusterAddslots(...$slots) Assign new hash slots to receiving node. + * @method mixed clusterCountkeysinslot($slot) Return the number of local keys in the specified hash slot. + * @method mixed clusterDelslots(...$slots) Set hash slots as unbound in receiving node. + * @method mixed clusterFailover($option = null) Forces a slave to perform a manual failover of its master.. + * @method mixed clusterForget($nodeId) Remove a node from the nodes table. + * @method mixed clusterGetkeysinslot($slot, $count) Return local key names in the specified hash slot. + * @method mixed clusterInfo() Provides info about Redis Cluster node state. + * @method mixed clusterKeyslot($key) Returns the hash slot of the specified key. + * @method mixed clusterMeet($ip, $port) Force a node cluster to handshake with another node. + * @method mixed clusterNodes() Get Cluster config for the node. + * @method mixed clusterReplicate($nodeId) Reconfigure a node as a slave of the specified master node. + * @method mixed clusterReset($resetType = "SOFT") Reset a Redis Cluster node. + * @method mixed clusterSaveconfig() Forces the node to save cluster state on disk. + * @method mixed clusterSetslot($slot, $type, $nodeid = null) Bind a hash slot to a specific node. + * @method mixed clusterSlaves($nodeId) List slave nodes of the specified master node. + * @method mixed clusterSlots() Get array of Cluster slot to node mappings. + * @method mixed command() Get array of Redis command details. + * @method mixed commandCount() Get total number of Redis commands. + * @method mixed commandGetkeys() Extract keys given a full Redis command. + * @method mixed commandInfo(...$commandNames) Get array of specific Redis command details. + * @method mixed configGet($parameter) Get the value of a configuration parameter. + * @method mixed configRewrite() Rewrite the configuration file with the in memory configuration. + * @method mixed configSet($parameter, $value) Set a configuration parameter to the given value. + * @method mixed configResetstat() Reset the stats returned by INFO. + * @method mixed dbsize() Return the number of keys in the selected database. + * @method mixed debugObject($key) Get debugging information about a key. + * @method mixed debugSegfault() Make the server crash. + * @method mixed decr($key) Decrement the integer value of a key by one. + * @method mixed decrby($key, $decrement) Decrement the integer value of a key by the given number. + * @method mixed del(...$keys) Delete a key. + * @method mixed discard() Discard all commands issued after MULTI. + * @method mixed dump($key) Return a serialized version of the value stored at the specified key.. + * @method mixed echo ($message) Echo the given string. + * @method mixed eval($script, $numkeys, ...$keys, ...$args) Execute a Lua script server side. + * @method mixed evalsha($sha1, $numkeys, ...$keys, ...$args) Execute a Lua script server side. + * @method mixed exec() Execute all commands issued after MULTI. + * @method mixed exists(...$keys) Determine if a key exists. + * @method mixed expire($key, $seconds) Set a key's time to live in seconds. + * @method mixed expireat($key, $timestamp) Set the expiration for a key as a UNIX timestamp. + * @method mixed flushall($ASYNC = null) Remove all keys from all databases. + * @method mixed flushdb($ASYNC = null) Remove all keys from the current database. + * @method mixed geoadd($key, $longitude, $latitude, $member, ...$more) Add one or more geospatial items in the geospatial index represented using a sorted set. + * @method mixed geohash($key, ...$members) Returns members of a geospatial index as standard geohash strings. + * @method mixed geopos($key, ...$members) Returns longitude and latitude of members of a geospatial index. + * @method mixed geodist($key, $member1, $member2, $unit = null) Returns the distance between two members of a geospatial index. + * @method mixed georadius($key, $longitude, $latitude, $radius, $metric, ...$options) Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point. + * @method mixed georadiusbymember($key, $member, $radius, $metric, ...$options) Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member. + * @method mixed get($key) Get the value of a key. + * @method mixed getbit($key, $offset) Returns the bit value at offset in the string value stored at key. + * @method mixed getrange($key, $start, $end) Get a substring of the string stored at a key. + * @method mixed getset($key, $value) Set the string value of a key and return its old value. + * @method mixed hdel($key, ...$fields) Delete one or more hash fields. + * @method mixed hexists($key, $field) Determine if a hash field exists. + * @method mixed hget($key, $field) Get the value of a hash field. + * @method mixed hgetall($key) Get all the fields and values in a hash. + * @method mixed hincrby($key, $field, $increment) Increment the integer value of a hash field by the given number. + * @method mixed hincrbyfloat($key, $field, $increment) Increment the float value of a hash field by the given amount. + * @method mixed hkeys($key) Get all the fields in a hash. + * @method mixed hlen($key) Get the number of fields in a hash. + * @method mixed hmget($key, ...$fields) Get the values of all the given hash fields. + * @method mixed hmset($key, $field, $value, ...$more) Set multiple hash fields to multiple values. + * @method mixed hset($key, $field, $value) Set the string value of a hash field. + * @method mixed hsetnx($key, $field, $value) Set the value of a hash field, only if the field does not exist. + * @method mixed hstrlen($key, $field) Get the length of the value of a hash field. + * @method mixed hvals($key) Get all the values in a hash. + * @method mixed incr($key) Increment the integer value of a key by one. + * @method mixed incrby($key, $increment) Increment the integer value of a key by the given amount. + * @method mixed incrbyfloat($key, $increment) Increment the float value of a key by the given amount. + * @method mixed info($section = null) Get information and statistics about the server. + * @method mixed keys($pattern) Find all keys matching the given pattern. + * @method mixed lastsave() Get the UNIX time stamp of the last successful save to disk. + * @method mixed lindex($key, $index) Get an element from a list by its index. + * @method mixed linsert($key, $where, $pivot, $value) Insert an element before or after another element in a list. + * @method mixed llen($key) Get the length of a list. + * @method mixed lpop($key) Remove and get the first element in a list. + * @method mixed lpush($key, ...$values) Prepend one or multiple values to a list. + * @method mixed lpushx($key, $value) Prepend a value to a list, only if the list exists. + * @method mixed lrange($key, $start, $stop) Get a range of elements from a list. + * @method mixed lrem($key, $count, $value) Remove elements from a list. + * @method mixed lset($key, $index, $value) Set the value of an element in a list by its index. + * @method mixed ltrim($key, $start, $stop) Trim a list to the specified range. + * @method mixed mget(...$keys) Get the values of all the given keys. + * @method mixed migrate($host, $port, $key, $destinationDb, $timeout, ...$options) Atomically transfer a key from a Redis instance to another one.. + * @method mixed monitor() Listen for all requests received by the server in real time. + * @method mixed move($key, $db) Move a key to another database. + * @method mixed mset(...$keyValuePairs) Set multiple keys to multiple values. + * @method mixed msetnx(...$keyValuePairs) Set multiple keys to multiple values, only if none of the keys exist. + * @method mixed multi() Mark the start of a transaction block. + * @method mixed object($subcommand, ...$argumentss) Inspect the internals of Redis objects. + * @method mixed persist($key) Remove the expiration from a key. + * @method mixed pexpire($key, $milliseconds) Set a key's time to live in milliseconds. + * @method mixed pexpireat($key, $millisecondsTimestamp) Set the expiration for a key as a UNIX timestamp specified in milliseconds. + * @method mixed pfadd($key, ...$elements) Adds the specified elements to the specified HyperLogLog.. + * @method mixed pfcount(...$keys) Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).. + * @method mixed pfmerge($destkey, ...$sourcekeys) Merge N different HyperLogLogs into a single one.. + * @method mixed ping($message = null) Ping the server. + * @method mixed psetex($key, $milliseconds, $value) Set the value and expiration in milliseconds of a key. + * @method mixed psubscribe(...$patterns) Listen for messages published to channels matching the given patterns. + * @method mixed pubsub($subcommand, ...$arguments) Inspect the state of the Pub/Sub subsystem. + * @method mixed pttl($key) Get the time to live for a key in milliseconds. + * @method mixed publish($channel, $message) Post a message to a channel. + * @method mixed punsubscribe(...$patterns) Stop listening for messages posted to channels matching the given patterns. + * @method mixed quit() Close the connection. + * @method mixed randomkey() Return a random key from the keyspace. + * @method mixed readonly() Enables read queries for a connection to a cluster slave node. + * @method mixed readwrite() Disables read queries for a connection to a cluster slave node. + * @method mixed rename($key, $newkey) Rename a key. + * @method mixed renamenx($key, $newkey) Rename a key, only if the new key does not exist. + * @method mixed restore($key, $ttl, $serializedValue, $REPLACE = null) Create a key using the provided serialized value, previously obtained using DUMP.. + * @method mixed role() Return the role of the instance in the context of replication. + * @method mixed rpop($key) Remove and get the last element in a list. + * @method mixed rpoplpush($source, $destination) Remove the last element in a list, prepend it to another list and return it. + * @method mixed rpush($key, ...$values) Append one or multiple values to a list. + * @method mixed rpushx($key, $value) Append a value to a list, only if the list exists. + * @method mixed sadd($key, ...$members) Add one or more members to a set. + * @method mixed save() Synchronously save the dataset to disk. + * @method mixed scard($key) Get the number of members in a set. + * @method mixed scriptDebug($option) Set the debug mode for executed scripts.. + * @method mixed scriptExists(...$sha1s) Check existence of scripts in the script cache.. + * @method mixed scriptFlush() Remove all the scripts from the script cache.. + * @method mixed scriptKill() Kill the script currently in execution.. + * @method mixed scriptLoad($script) Load the specified Lua script into the script cache.. + * @method mixed sdiff(...$keys) Subtract multiple sets. + * @method mixed sdiffstore($destination, ...$keys) Subtract multiple sets and store the resulting set in a key. + * @method mixed select($index) Change the selected database for the current connection. + * @method mixed set($key, $value, ...$options) Set the string value of a key. + * @method mixed setbit($key, $offset, $value) Sets or clears the bit at offset in the string value stored at key. + * @method mixed setex($key, $seconds, $value) Set the value and expiration of a key. + * @method mixed setnx($key, $value) Set the value of a key, only if the key does not exist. + * @method mixed setrange($key, $offset, $value) Overwrite part of a string at key starting at the specified offset. + * @method mixed shutdown($saveOption = null) Synchronously save the dataset to disk and then shut down the server. + * @method mixed sinter(...$keys) Intersect multiple sets. + * @method mixed sinterstore($destination, ...$keys) Intersect multiple sets and store the resulting set in a key. + * @method mixed sismember($key, $member) Determine if a given value is a member of a set. + * @method mixed slaveof($host, $port) Make the server a slave of another instance, or promote it as master. + * @method mixed slowlog($subcommand, $argument = null) Manages the Redis slow queries log. + * @method mixed smembers($key) Get all the members in a set. + * @method mixed smove($source, $destination, $member) Move a member from one set to another. + * @method mixed sort($key, ...$options) Sort the elements in a list, set or sorted set. + * @method mixed spop($key, $count = null) Remove and return one or multiple random members from a set. + * @method mixed srandmember($key, $count = null) Get one or multiple random members from a set. + * @method mixed srem($key, ...$members) Remove one or more members from a set. + * @method mixed strlen($key) Get the length of the value stored in a key. + * @method mixed subscribe(...$channels) Listen for messages published to the given channels. + * @method mixed sunion(...$keys) Add multiple sets. + * @method mixed sunionstore($destination, ...$keys) Add multiple sets and store the resulting set in a key. + * @method mixed swapdb($index, $index) Swaps two Redis databases. + * @method mixed sync() Internal command used for replication. + * @method mixed time() Return the current server time. + * @method mixed touch(...$keys) Alters the last access time of a key(s). Returns the number of existing keys specified.. + * @method mixed ttl($key) Get the time to live for a key. + * @method mixed type($key) Determine the type stored at key. + * @method mixed unsubscribe(...$channels) Stop listening for messages posted to the given channels. + * @method mixed unlink(...$keys) Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.. + * @method mixed unwatch() Forget about all watched keys. + * @method mixed wait($numslaves, $timeout) Wait for the synchronous replication of all the write commands sent in the context of the current connection. + * @method mixed watch(...$keys) Watch the given keys to determine execution of the MULTI/EXEC block. + * @method mixed xack($stream, $group, ...$ids) Removes one or multiple messages from the pending entries list (PEL) of a stream consumer group + * @method mixed xadd($stream, $id, $field, $value, ...$fieldsValues) Appends the specified stream entry to the stream at the specified key + * @method mixed xclaim($stream, $group, $consumer, $minIdleTimeMs, $id, ...$options) Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument + * @method mixed xdel($stream, ...$ids) Removes the specified entries from a stream, and returns the number of entries deleted + * @method mixed xgroup($subCommand, $stream, $group, ...$options) Manages the consumer groups associated with a stream data structure + * @method mixed xinfo($subCommand, $stream, ...$options) Retrieves different information about the streams and associated consumer groups + * @method mixed xlen($stream) Returns the number of entries inside a stream + * @method mixed xpending($stream, $group, ...$options) Fetching data from a stream via a consumer group, and not acknowledging such data, has the effect of creating pending entries + * @method mixed xrange($stream, $start, $end, ...$options) Returns the stream entries matching a given range of IDs + * @method mixed xread(...$options) Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller + * @method mixed xreadgroup($subCommand, $group, $consumer, ...$options) Special version of the XREAD command with support for consumer groups + * @method mixed xrevrange($stream, $end, $start, ...$options) Exactly like XRANGE, but with the notable difference of returning the entries in reverse order, and also taking the start-end range in reverse order + * @method mixed xtrim($stream, $strategy, ...$options) Trims the stream to a given number of items, evicting older items (items with lower IDs) if needed + * @method mixed zadd($key, ...$options) Add one or more members to a sorted set, or update its score if it already exists. + * @method mixed zcard($key) Get the number of members in a sorted set. + * @method mixed zcount($key, $min, $max) Count the members in a sorted set with scores within the given values. + * @method mixed zincrby($key, $increment, $member) Increment the score of a member in a sorted set. + * @method mixed zinterstore($destination, $numkeys, $key, ...$options) Intersect multiple sorted sets and store the resulting sorted set in a new key. + * @method mixed zlexcount($key, $min, $max) Count the number of members in a sorted set between a given lexicographical range. + * @method mixed zrange($key, $start, $stop, $WITHSCORES = null) Return a range of members in a sorted set, by index. + * @method mixed zrangebylex($key, $min, $max, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by lexicographical range. + * @method mixed zrevrangebylex($key, $max, $min, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.. + * @method mixed zrangebyscore($key, $min, $max, ...$options) Return a range of members in a sorted set, by score. + * @method mixed zrank($key, $member) Determine the index of a member in a sorted set. + * @method mixed zrem($key, ...$members) Remove one or more members from a sorted set. + * @method mixed zremrangebylex($key, $min, $max) Remove all members in a sorted set between the given lexicographical range. + * @method mixed zremrangebyrank($key, $start, $stop) Remove all members in a sorted set within the given indexes. + * @method mixed zremrangebyscore($key, $min, $max) Remove all members in a sorted set within the given scores. + * @method mixed zrevrange($key, $start, $stop, $WITHSCORES = null) Return a range of members in a sorted set, by index, with scores ordered from high to low. + * @method mixed zrevrangebyscore($key, $max, $min, $WITHSCORES = null, $LIMIT = null, $offset = null, $count = null) Return a range of members in a sorted set, by score, with scores ordered from high to low. + * @method mixed zrevrank($key, $member) Determine the index of a member in a sorted set, with scores ordered from high to low. + * @method mixed zscore($key, $member) Get the score associated with the given member in a sorted set. + * @method mixed zunionstore($destination, $numkeys, $key, ...$options) Add multiple sorted sets and store the resulting sorted set in a new key. + * @method mixed scan($cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate the keys space. + * @method mixed sscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate Set elements. + * @method mixed hscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate hash fields and associated values. + * @method mixed zscan($key, $cursor, $MATCH = null, $pattern = null, $COUNT = null, $count = null) Incrementally iterate sorted sets elements and associated scores. + */ +interface ConnectionInterface +{ + public function open(): void; + + public function close(): void; + + public function getIsActive(): bool; + + public function executeCommand($name, $params = []): mixed; +} diff --git a/src/Mutex.php b/src/Mutex.php index 78a8d6cb4..f39c22c4c 100644 --- a/src/Mutex.php +++ b/src/Mutex.php @@ -73,7 +73,7 @@ class Mutex extends \yii\mutex\Mutex */ public $keyPrefix; /** - * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * @var ConnectionInterface|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure * redis connection as an application component. * After the Mutex object is created, if you want to change this property, you should only assign it @@ -95,7 +95,7 @@ class Mutex extends \yii\mutex\Mutex public function init() { parent::init(); - $this->redis = Instance::ensure($this->redis, Connection::className()); + $this->redis = Instance::ensure($this->redis, ConnectionInterface::class); if ($this->keyPrefix === null) { $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); } diff --git a/src/Session.php b/src/Session.php index 893b27df6..097867156 100644 --- a/src/Session.php +++ b/src/Session.php @@ -57,7 +57,7 @@ class Session extends \yii\web\Session { /** - * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * @var ConnectionInterface|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure * redis connection as an application component. * After the Session object is created, if you want to change this property, you should only assign it @@ -80,7 +80,7 @@ class Session extends \yii\web\Session */ public function init() { - $this->redis = Instance::ensure($this->redis, Connection::className()); + $this->redis = Instance::ensure($this->redis, ConnectionInterface::class); if ($this->keyPrefix === null) { $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); } diff --git a/src/predis/PredisConnection.php b/src/predis/PredisConnection.php index 0f14977c1..c33544c5f 100644 --- a/src/predis/PredisConnection.php +++ b/src/predis/PredisConnection.php @@ -7,9 +7,11 @@ use Predis\Response\ErrorInterface; use Predis\Response\ResponseInterface; use Yii; +use yii\base\Component; use yii\base\InvalidConfigException; use yii\helpers\Inflector; -use yii\redis\Connection as YiiRedisConnection; +use yii\redis\ConnectionInterface; +use yii\redis\LuaScriptBuilder; use yii\redis\predis\Command\CommandDecorator; /** @@ -63,8 +65,249 @@ * ]; * ``` */ -class PredisConnection extends YiiRedisConnection +class PredisConnection extends Component implements ConnectionInterface { + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; + + /** + * @var array List of available redis commands. + * @see https://redis.io/commands + */ + public $redisCommands = [ + 'APPEND', // Append a value to a key + 'AUTH', // Authenticate to the server + 'BGREWRITEAOF', // Asynchronously rewrite the append-only file + 'BGSAVE', // Asynchronously save the dataset to disk + 'BITCOUNT', // Count set bits in a string + 'BITFIELD', // Perform arbitrary bitfield integer operations on strings + 'BITOP', // Perform bitwise operations between strings + 'BITPOS', // Find first bit set or clear in a string + 'BLPOP', // Remove and get the first element in a list, or block until one is available + 'BRPOP', // Remove and get the last element in a list, or block until one is available + 'BRPOPLPUSH', // Pop a value from a list, push it to another list and return it; or block until one is available + 'CLIENT KILL', // Kill the connection of a client + 'CLIENT LIST', // Get the list of client connections + 'CLIENT GETNAME', // Get the current connection name + 'CLIENT PAUSE', // Stop processing commands from clients for some time + 'CLIENT REPLY', // Instruct the server whether to reply to commands + 'CLIENT SETNAME', // Set the current connection name + 'CLUSTER ADDSLOTS', // Assign new hash slots to receiving node + 'CLUSTER COUNTKEYSINSLOT', // Return the number of local keys in the specified hash slot + 'CLUSTER DELSLOTS', // Set hash slots as unbound in receiving node + 'CLUSTER FAILOVER', // Forces a slave to perform a manual failover of its master. + 'CLUSTER FORGET', // Remove a node from the nodes table + 'CLUSTER GETKEYSINSLOT', // Return local key names in the specified hash slot + 'CLUSTER INFO', // Provides info about Redis Cluster node state + 'CLUSTER KEYSLOT', // Returns the hash slot of the specified key + 'CLUSTER MEET', // Force a node cluster to handshake with another node + 'CLUSTER NODES', // Get Cluster config for the node + 'CLUSTER REPLICATE', // Reconfigure a node as a slave of the specified master node + 'CLUSTER RESET', // Reset a Redis Cluster node + 'CLUSTER SAVECONFIG', // Forces the node to save cluster state on disk + 'CLUSTER SETSLOT', // Bind a hash slot to a specific node + 'CLUSTER SLAVES', // List slave nodes of the specified master node + 'CLUSTER SLOTS', // Get array of Cluster slot to node mappings + 'COMMAND', // Get array of Redis command details + 'COMMAND COUNT', // Get total number of Redis commands + 'COMMAND GETKEYS', // Extract keys given a full Redis command + 'COMMAND INFO', // Get array of specific Redis command details + 'CONFIG GET', // Get the value of a configuration parameter + 'CONFIG REWRITE', // Rewrite the configuration file with the in memory configuration + 'CONFIG SET', // Set a configuration parameter to the given value + 'CONFIG RESETSTAT', // Reset the stats returned by INFO + 'DBSIZE', // Return the number of keys in the selected database + 'DEBUG OBJECT', // Get debugging information about a key + 'DEBUG SEGFAULT', // Make the server crash + 'DECR', // Decrement the integer value of a key by one + 'DECRBY', // Decrement the integer value of a key by the given number + 'DEL', // Delete a key + 'DISCARD', // Discard all commands issued after MULTI + 'DUMP', // Return a serialized version of the value stored at the specified key. + 'ECHO', // Echo the given string + 'EVAL', // Execute a Lua script server side + 'EVALSHA', // Execute a Lua script server side + 'EXEC', // Execute all commands issued after MULTI + 'EXISTS', // Determine if a key exists + 'EXPIRE', // Set a key's time to live in seconds + 'EXPIREAT', // Set the expiration for a key as a UNIX timestamp + 'FLUSHALL', // Remove all keys from all databases + 'FLUSHDB', // Remove all keys from the current database + 'GEOADD', // Add one or more geospatial items in the geospatial index represented using a sorted set + 'GEOHASH', // Returns members of a geospatial index as standard geohash strings + 'GEOPOS', // Returns longitude and latitude of members of a geospatial index + 'GEODIST', // Returns the distance between two members of a geospatial index + 'GEORADIUS', // Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point + 'GEORADIUSBYMEMBER', // Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member + 'GET', // Get the value of a key + 'GETBIT', // Returns the bit value at offset in the string value stored at key + 'GETRANGE', // Get a substring of the string stored at a key + 'GETSET', // Set the string value of a key and return its old value + 'HDEL', // Delete one or more hash fields + 'HEXISTS', // Determine if a hash field exists + 'HGET', // Get the value of a hash field + 'HGETALL', // Get all the fields and values in a hash + 'HINCRBY', // Increment the integer value of a hash field by the given number + 'HINCRBYFLOAT', // Increment the float value of a hash field by the given amount + 'HKEYS', // Get all the fields in a hash + 'HLEN', // Get the number of fields in a hash + 'HMGET', // Get the values of all the given hash fields + 'HMSET', // Set multiple hash fields to multiple values + 'HSET', // Set the string value of a hash field + 'HSETNX', // Set the value of a hash field, only if the field does not exist + 'HSTRLEN', // Get the length of the value of a hash field + 'HVALS', // Get all the values in a hash + 'INCR', // Increment the integer value of a key by one + 'INCRBY', // Increment the integer value of a key by the given amount + 'INCRBYFLOAT', // Increment the float value of a key by the given amount + 'INFO', // Get information and statistics about the server + 'KEYS', // Find all keys matching the given pattern + 'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk + 'LINDEX', // Get an element from a list by its index + 'LINSERT', // Insert an element before or after another element in a list + 'LLEN', // Get the length of a list + 'LPOP', // Remove and get the first element in a list + 'LPUSH', // Prepend one or multiple values to a list + 'LPUSHX', // Prepend a value to a list, only if the list exists + 'LRANGE', // Get a range of elements from a list + 'LREM', // Remove elements from a list + 'LSET', // Set the value of an element in a list by its index + 'LTRIM', // Trim a list to the specified range + 'MGET', // Get the values of all the given keys + 'MIGRATE', // Atomically transfer a key from a Redis instance to another one. + 'MONITOR', // Listen for all requests received by the server in real time + 'MOVE', // Move a key to another database + 'MSET', // Set multiple keys to multiple values + 'MSETNX', // Set multiple keys to multiple values, only if none of the keys exist + 'MULTI', // Mark the start of a transaction block + 'OBJECT', // Inspect the internals of Redis objects + 'PERSIST', // Remove the expiration from a key + 'PEXPIRE', // Set a key's time to live in milliseconds + 'PEXPIREAT', // Set the expiration for a key as a UNIX timestamp specified in milliseconds + 'PFADD', // Adds the specified elements to the specified HyperLogLog. + 'PFCOUNT', // Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s). + 'PFMERGE', // Merge N different HyperLogLogs into a single one. + 'PING', // Ping the server + 'PSETEX', // Set the value and expiration in milliseconds of a key + 'PSUBSCRIBE', // Listen for messages published to channels matching the given patterns + 'PUBSUB', // Inspect the state of the Pub/Sub subsystem + 'PTTL', // Get the time to live for a key in milliseconds + 'PUBLISH', // Post a message to a channel + 'PUNSUBSCRIBE', // Stop listening for messages posted to channels matching the given patterns + 'QUIT', // Close the connection + 'RANDOMKEY', // Return a random key from the keyspace + 'READONLY', // Enables read queries for a connection to a cluster slave node + 'READWRITE', // Disables read queries for a connection to a cluster slave node + 'RENAME', // Rename a key + 'RENAMENX', // Rename a key, only if the new key does not exist + 'RESTORE', // Create a key using the provided serialized value, previously obtained using DUMP. + 'ROLE', // Return the role of the instance in the context of replication + 'RPOP', // Remove and get the last element in a list + 'RPOPLPUSH', // Remove the last element in a list, prepend it to another list and return it + 'RPUSH', // Append one or multiple values to a list + 'RPUSHX', // Append a value to a list, only if the list exists + 'SADD', // Add one or more members to a set + 'SAVE', // Synchronously save the dataset to disk + 'SCARD', // Get the number of members in a set + 'SCRIPT DEBUG', // Set the debug mode for executed scripts. + 'SCRIPT EXISTS', // Check existence of scripts in the script cache. + 'SCRIPT FLUSH', // Remove all the scripts from the script cache. + 'SCRIPT KILL', // Kill the script currently in execution. + 'SCRIPT LOAD', // Load the specified Lua script into the script cache. + 'SDIFF', // Subtract multiple sets + 'SDIFFSTORE', // Subtract multiple sets and store the resulting set in a key + 'SELECT', // Change the selected database for the current connection + 'SET', // Set the string value of a key + 'SETBIT', // Sets or clears the bit at offset in the string value stored at key + 'SETEX', // Set the value and expiration of a key + 'SETNX', // Set the value of a key, only if the key does not exist + 'SETRANGE', // Overwrite part of a string at key starting at the specified offset + 'SHUTDOWN', // Synchronously save the dataset to disk and then shut down the server + 'SINTER', // Intersect multiple sets + 'SINTERSTORE', // Intersect multiple sets and store the resulting set in a key + 'SISMEMBER', // Determine if a given value is a member of a set + 'SLAVEOF', // Make the server a slave of another instance, or promote it as master + 'SLOWLOG', // Manages the Redis slow queries log + 'SMEMBERS', // Get all the members in a set + 'SMOVE', // Move a member from one set to another + 'SORT', // Sort the elements in a list, set or sorted set + 'SPOP', // Remove and return one or multiple random members from a set + 'SRANDMEMBER', // Get one or multiple random members from a set + 'SREM', // Remove one or more members from a set + 'STRLEN', // Get the length of the value stored in a key + 'SUBSCRIBE', // Listen for messages published to the given channels + 'SUNION', // Add multiple sets + 'SUNIONSTORE', // Add multiple sets and store the resulting set in a key + 'SWAPDB', // Swaps two Redis databases + 'SYNC', // Internal command used for replication + 'TIME', // Return the current server time + 'TOUCH', // Alters the last access time of a key(s). Returns the number of existing keys specified. + 'TTL', // Get the time to live for a key + 'TYPE', // Determine the type stored at key + 'UNSUBSCRIBE', // Stop listening for messages posted to the given channels + 'UNLINK', // Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking. + 'UNWATCH', // Forget about all watched keys + 'WAIT', // Wait for the synchronous replication of all the write commands sent in the context of the current connection + 'WATCH', // Watch the given keys to determine execution of the MULTI/EXEC block + 'XACK', // Removes one or multiple messages from the pending entries list (PEL) of a stream consumer group + 'XADD', // Appends the specified stream entry to the stream at the specified key + 'XCLAIM', // Changes the ownership of a pending message, so that the new owner is the consumer specified as the command argument + 'XDEL', // Removes the specified entries from a stream, and returns the number of entries deleted + 'XGROUP', // Manages the consumer groups associated with a stream data structure + 'XINFO', // Retrieves different information about the streams and associated consumer groups + 'XLEN', // Returns the number of entries inside a stream + 'XPENDING', // Fetching data from a stream via a consumer group, and not acknowledging such data, has the effect of creating pending entries + 'XRANGE', // Returns the stream entries matching a given range of IDs + 'XREAD', // Read data from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller + 'XREADGROUP', // Special version of the XREAD command with support for consumer groups + 'XREVRANGE', // Exactly like XRANGE, but with the notable difference of returning the entries in reverse order, and also taking the start-end range in reverse order + 'XTRIM', // Trims the stream to a given number of items, evicting older items (items with lower IDs) if needed + 'ZADD', // Add one or more members to a sorted set, or update its score if it already exists + 'ZCARD', // Get the number of members in a sorted set + 'ZCOUNT', // Count the members in a sorted set with scores within the given values + 'ZINCRBY', // Increment the score of a member in a sorted set + 'ZINTERSTORE', // Intersect multiple sorted sets and store the resulting sorted set in a new key + 'ZLEXCOUNT', // Count the number of members in a sorted set between a given lexicographical range + 'ZRANGE', // Return a range of members in a sorted set, by index + 'ZRANGEBYLEX', // Return a range of members in a sorted set, by lexicographical range + 'ZREVRANGEBYLEX', // Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings. + 'ZRANGEBYSCORE', // Return a range of members in a sorted set, by score + 'ZRANK', // Determine the index of a member in a sorted set + 'ZREM', // Remove one or more members from a sorted set + 'ZREMRANGEBYLEX', // Remove all members in a sorted set between the given lexicographical range + 'ZREMRANGEBYRANK', // Remove all members in a sorted set within the given indexes + 'ZREMRANGEBYSCORE', // Remove all members in a sorted set within the given scores + 'ZREVRANGE', // Return a range of members in a sorted set, by index, with scores ordered from high to low + 'ZREVRANGEBYSCORE', // Return a range of members in a sorted set, by score, with scores ordered from high to low + 'ZREVRANK', // Determine the index of a member in a sorted set, with scores ordered from high to low + 'ZSCORE', // Get the score associated with the given member in a sorted set + 'ZUNIONSTORE', // Add multiple sorted sets and store the resulting sorted set in a new key + 'SCAN', // Incrementally iterate the keys space + 'SSCAN', // Incrementally iterate Set elements + 'HSCAN', // Incrementally iterate hash fields and associated values + 'ZSCAN', // Incrementally iterate sorted sets elements and associated scores + ]; + + /** + * @return LuaScriptBuilder + */ + public function getLuaScriptBuilder() + { + return new LuaScriptBuilder(); + } + + /** + * Initializes the DB connection. + * This method is invoked right after the DB connection is established. + * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. + */ + protected function initConnection() + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + /** * @var mixed Connection parameters for one or more servers. */ @@ -91,7 +334,6 @@ public function getIsActive(): bool } /** - * @inheritdoc * @return mixed|ErrorInterface|ResponseInterface * @throws InvalidConfigException */ @@ -137,7 +379,6 @@ public function open(): void } /** - * @inheritdoc */ public function close(): void { @@ -158,7 +399,6 @@ public function getClientSocket(): ?Client } /** - * @inheritdoc */ public function ping($message = null): bool { From ddd84670dfd91b84eb880ffa406d0b6173509f8c Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 1 Jul 2025 19:25:19 +0300 Subject: [PATCH 08/49] ConnectionInterface --- src/Cache.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cache.php b/src/Cache.php index f55c003c8..1195cd36f 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -101,7 +101,7 @@ class Cache extends \yii\caching\Cache { /** - * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. + * @var ConnectionInterface|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure * redis connection as an application component. * After the Cache object is created, if you want to change this property, you should only assign it @@ -151,7 +151,7 @@ class Cache extends \yii\caching\Cache public $shareDatabase = false; /** - * @var Connection currently active connection. + * @var ConnectionInterface currently active connection. */ private $_replica; /** @@ -172,7 +172,7 @@ class Cache extends \yii\caching\Cache public function init() { parent::init(); - $this->redis = Instance::ensure($this->redis, Connection::className()); + $this->redis = Instance::ensure($this->redis, ConnectionInterface::class); } /** @@ -371,7 +371,7 @@ protected function flushValues() * It will return the current Replica Redis [[Connection]], and fall back to default [[redis]] [[Connection]] * defined in this instance. Only used in getValue() and getValues(). * @since 2.0.8 - * @return array|string|Connection + * @return array|string|ConnectionInterface * @throws \yii\base\InvalidConfigException */ protected function getReplica() From 9d8d0599f9cbe39ae7f6116eb4c0abe5cccd236d Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 13:55:37 +0300 Subject: [PATCH 09/49] fix test --- tests/RedisCacheTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/RedisCacheTest.php b/tests/RedisCacheTest.php index 548727a5a..162ae1507 100644 --- a/tests/RedisCacheTest.php +++ b/tests/RedisCacheTest.php @@ -202,6 +202,7 @@ public function testReplica() public function testFlushWithSharedDatabase() { $instance = $this->getCacheInstance(); + $this->resetCacheInstance(); $instance->shareDatabase = true; $instance->keyPrefix = 'myprefix_'; $instance->redis->set('testkey', 'testvalue'); From 6a1e9164fdc251f204ddccb00991bbddaa4b3238 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 13:56:24 +0300 Subject: [PATCH 10/49] PredisConnection --- src/predis/PredisConnection.php | 66 +++++++---------- tests/predis/cluster/config/config.php | 30 ++++++++ tests/predis/sentinel/RedisCacheTest.php | 73 +------------------ tests/predis/sentinel/RedisConnectionTest.php | 2 +- tests/predis/sentinel/config/config.php | 1 - tests/predis/standalone/RedisCacheTest.php | 73 +------------------ .../predis/standalone/RedisConnectionTest.php | 2 +- tests/predis/standalone/config/config.php | 1 - 8 files changed, 60 insertions(+), 188 deletions(-) create mode 100644 tests/predis/cluster/config/config.php diff --git a/src/predis/PredisConnection.php b/src/predis/PredisConnection.php index c33544c5f..66f3ac522 100644 --- a/src/predis/PredisConnection.php +++ b/src/predis/PredisConnection.php @@ -6,6 +6,7 @@ use Predis\Client; use Predis\Response\ErrorInterface; use Predis\Response\ResponseInterface; +use Predis\Response\Status; use Yii; use yii\base\Component; use yii\base\InvalidConfigException; @@ -29,9 +30,6 @@ * ], * 'options' => [ * 'replication' => 'sentinel', - * // значение для 'service' можно получить подключившись к redis - * // через redis-cli -h 127.0.0.1 -p 26379 --pass 'password' - * // и выполнить SENTINEL masters * 'service' => 'mymaster', * 'parameters' => [ * 'password' => 'password', @@ -40,7 +38,6 @@ * 'persistent' => true, // performs the connection asynchronously * 'async_connect' => true, //the connection asynchronously * 'read_write_timeout' => 0.1, // timeout of read / write operations - * //'timeout' => 0.1, // @note timeout переопределяется в predis, timeout в строке подключения * ], * ], * ]; @@ -60,7 +57,6 @@ * 'persistent' => true, // performs the connection asynchronously * 'async_connect' => true, //the connection asynchronously * 'read_write_timeout' => 0.1, // timeout of read / write operations - * //'timeout' => 0.1, // @note timeout переопределяется в predis, используй timeout в строке подключения * ], * ]; * ``` @@ -70,13 +66,13 @@ class PredisConnection extends Component implements ConnectionInterface /** * @event Event an event that is triggered after a DB connection is established */ - const EVENT_AFTER_OPEN = 'afterOpen'; + public const EVENT_AFTER_OPEN = 'afterOpen'; /** * @var array List of available redis commands. * @see https://redis.io/commands */ - public $redisCommands = [ + public array $redisCommands = [ 'APPEND', // Append a value to a key 'AUTH', // Authenticate to the server 'BGREWRITEAOF', // Asynchronously rewrite the append-only file @@ -293,7 +289,7 @@ class PredisConnection extends Component implements ConnectionInterface /** * @return LuaScriptBuilder */ - public function getLuaScriptBuilder() + public function getLuaScriptBuilder(): LuaScriptBuilder { return new LuaScriptBuilder(); } @@ -303,7 +299,7 @@ public function getLuaScriptBuilder() * This method is invoked right after the DB connection is established. * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. */ - protected function initConnection() + protected function initConnection(): void { $this->trigger(self::EVENT_AFTER_OPEN); } @@ -321,7 +317,7 @@ protected function initConnection() /** * @var Client|null redis connection */ - protected Client|null $clientSocket = null; + protected Client|null $client = null; /** * Returns a value indicating whether the DB connection is established. @@ -330,7 +326,7 @@ protected function initConnection() */ public function getIsActive(): bool { - return (bool)$this->clientSocket?->isConnected(); + return (bool)$this->client?->isConnected(); } /** @@ -341,14 +337,14 @@ public function executeCommand($name, $params = []): mixed { $this->open(); - Yii::debug("Executing Redis Command: {$name} " . implode(' ', $params), __METHOD__); + Yii::debug("Executing Redis Command: $name " . implode(' ', $params), __METHOD__); -// $aaa = $this->database; -// $this->clientSocket->select(1); - - $command = $this->clientSocket->createCommand($name, $params); - $res = $this->clientSocket->executeCommand(new CommandDecorator($command)); - return $res; + $command = $this->client->createCommand($name, $params); + $response = $this->client->executeCommand(new CommandDecorator($command)); + if ($response instanceof Status) { + return (string)$response === 'OK' || (string)$response === 'PONG'; + } + return $response; } /** @@ -359,7 +355,7 @@ public function executeCommand($name, $params = []): mixed */ public function open(): void { - if (null !== $this->clientSocket) { + if (null !== $this->client) { return; } @@ -369,20 +365,18 @@ public function open(): void Yii::debug('Opening redis DB connection', __METHOD__); -// $otp = array_merge([ -// 'commands' => new CommandFactory(), -// $this->options, -// ]); - - $this->clientSocket = new Client($this->parameters, $this->options); + $this->client = new Client($this->parameters, $this->options); $this->initConnection(); } + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. */ public function close(): void { - $this->clientSocket?->disconnect(); + $this->client?->disconnect(); } /** @@ -391,19 +385,11 @@ public function close(): void * @return Client|null * @throws InvalidConfigException */ - public function getClientSocket(): ?Client + public function getClient(): ?Client { $this->open(); - return $this->clientSocket; - } - - /** - */ - public function ping($message = null): bool - { - $this->open(); - return (string)$this->clientSocket->ping() === 'PONG'; + return $this->client; } /** @@ -415,13 +401,13 @@ public function ping($message = null): bool * @param string $name name of the missing method to execute * @param array $params method call arguments * @return mixed + * @throws InvalidConfigException */ - public function __call($name, $params) + public function __call($name, $params): mixed { $redisCommand = strtoupper(Inflector::camel2words($name, false)); - if (in_array($redisCommand, $this->redisCommands)) { - $res = $this->executeCommand($redisCommand, $params); - return $res; + if (in_array($redisCommand, $this->redisCommands, true)) { + return $this->executeCommand($redisCommand, $params); } return parent::__call($name, $params); diff --git a/tests/predis/cluster/config/config.php b/tests/predis/cluster/config/config.php new file mode 100644 index 000000000..c7b26fe3e --- /dev/null +++ b/tests/predis/cluster/config/config.php @@ -0,0 +1,30 @@ +ping(); + + +$config = [ + 'databases' => [ + 'redis' => [ +// 'parameters' => ['tcp://redis-cluster:7000', 'tcp://redis-cluster:7001', 'tcp://redis-cluster:7002'], + 'parameters' => [ + 'tcp://redis-node-0:6379', 'tcp://redis-node-1:6379', 'tcp://redis-node-2:6379', + ], + 'options' => [ + 'cluster' => 'redis', + 'parameters' => [ + 'persistent' => true, + 'conn_uid' => 'id_cluster', + ], + ], + ], + ], +]; + +return $config; diff --git a/tests/predis/sentinel/RedisCacheTest.php b/tests/predis/sentinel/RedisCacheTest.php index 1d4caa969..a67a0b49b 100644 --- a/tests/predis/sentinel/RedisCacheTest.php +++ b/tests/predis/sentinel/RedisCacheTest.php @@ -129,81 +129,10 @@ public function testMultiByteGetAndSet() $this->assertSame($cache->get($key), $data); } - public function testReplica() - { - $this->resetCacheInstance(); - - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $key = 'replica-1'; - $value = 'replica'; - - //No Replica listed - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - $databases = TestCase::getParam('databases'); - $redis = isset($databases['redis']) ? $databases['redis'] : null; - - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertSame($cache->get($key), $value); - - //One Replica listed - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - //Multiple Replicas listed - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - //invalid config - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $cache->replicas = ['redis']; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - $this->resetCacheInstance(); - } - public function testFlushWithSharedDatabase() { $instance = $this->getCacheInstance(); + $this->resetCacheInstance(); $instance->shareDatabase = true; $instance->keyPrefix = 'myprefix_'; $instance->redis->set('testkey', 'testvalue'); diff --git a/tests/predis/sentinel/RedisConnectionTest.php b/tests/predis/sentinel/RedisConnectionTest.php index 78bafc0a1..221fe925e 100644 --- a/tests/predis/sentinel/RedisConnectionTest.php +++ b/tests/predis/sentinel/RedisConnectionTest.php @@ -32,7 +32,7 @@ public function testConnect(): void $db->close(); $db = $this->getConnection(false); - $db->getClientSocket()->select(1); + $db->getClient()->select(1); $db->open(); $this->assertNull($db->get('YIITESTKEY')); $db->close(); diff --git a/tests/predis/sentinel/config/config.php b/tests/predis/sentinel/config/config.php index 37e4882ec..6805e0480 100644 --- a/tests/predis/sentinel/config/config.php +++ b/tests/predis/sentinel/config/config.php @@ -8,7 +8,6 @@ $config = [ 'databases' => [ 'redis' => [ -// 'class' => PredisConnection::class, 'parameters' => ['tcp://redis-sentinel-1:26379', 'tcp://redis-sentinel-2:26379', 'tcp://redis-sentinel-3:26379'], 'options' => [ 'replication' => 'sentinel', diff --git a/tests/predis/standalone/RedisCacheTest.php b/tests/predis/standalone/RedisCacheTest.php index 666164c5d..522194199 100644 --- a/tests/predis/standalone/RedisCacheTest.php +++ b/tests/predis/standalone/RedisCacheTest.php @@ -128,81 +128,10 @@ public function testMultiByteGetAndSet() $this->assertSame($cache->get($key), $data); } - public function testReplica() - { - $this->resetCacheInstance(); - - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $key = 'replica-1'; - $value = 'replica'; - - //No Replica listed - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - $databases = TestCase::getParam('databases'); - $redis = isset($databases['redis']) ? $databases['redis'] : null; - - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertSame($cache->get($key), $value); - - //One Replica listed - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - //Multiple Replicas listed - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $cache->replicas = [ - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - [ - 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost', - 'password' => isset($redis['password']) ? $redis['password'] : null, - ], - ]; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - //invalid config - $this->resetCacheInstance(); - $cache = $this->getCacheInstance(); - $cache->enableReplicas = true; - - $cache->replicas = ['redis']; - $this->assertFalse($cache->get($key)); - $cache->set($key, $value); - $this->assertSame($cache->get($key), $value); - - $this->resetCacheInstance(); - } - public function testFlushWithSharedDatabase() { $instance = $this->getCacheInstance(); + $this->resetCacheInstance(); $instance->shareDatabase = true; $instance->keyPrefix = 'myprefix_'; $instance->redis->set('testkey', 'testvalue'); diff --git a/tests/predis/standalone/RedisConnectionTest.php b/tests/predis/standalone/RedisConnectionTest.php index a8435e12f..c1358cb6f 100644 --- a/tests/predis/standalone/RedisConnectionTest.php +++ b/tests/predis/standalone/RedisConnectionTest.php @@ -32,7 +32,7 @@ public function testConnect(): void $db->close(); $db = $this->getConnection(false); - $db->getClientSocket()->select(1); + $db->getClient()->select(1); $db->open(); $this->assertNull($db->get('YIITESTKEY')); $db->close(); diff --git a/tests/predis/standalone/config/config.php b/tests/predis/standalone/config/config.php index 9fc417b91..e940c668d 100644 --- a/tests/predis/standalone/config/config.php +++ b/tests/predis/standalone/config/config.php @@ -8,7 +8,6 @@ $config = [ 'databases' => [ 'redis' => [ -// 'class' => PredisConnection::class, 'parameters' => 'tcp://redis:6379', 'options' => [ 'parameters' => [ From a16bbf64b85db1f25f60b0fd51d7a837c5287fca Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 13:57:19 +0300 Subject: [PATCH 11/49] predis on supported replicas --- src/Cache.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Cache.php b/src/Cache.php index 1195cd36f..c09905b16 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -7,7 +7,6 @@ namespace yii\redis; -use Yii; use yii\db\Exception; use yii\di\Instance; @@ -372,7 +371,7 @@ protected function flushValues() * defined in this instance. Only used in getValue() and getValues(). * @since 2.0.8 * @return array|string|ConnectionInterface - * @throws \yii\base\InvalidConfigException + * @throws \yii\base\InvalidConfigException|\yii\base\NotSupportedException */ protected function getReplica() { @@ -388,12 +387,16 @@ protected function getReplica() return $this->_replica = $this->redis; } + if ($this->redis instanceof \yii\redis\Predis\PredisConnection) { + throw new \yii\base\NotSupportedException( + 'predis on supported replicas', + ); + } + $replicas = $this->replicas; shuffle($replicas); $config = array_shift($replicas); - $class = $config['class'] ?? 'yii\redis\Connection'; - $connection = Yii::createObject($class, $config); - $this->_replica = Instance::ensure($connection, ConnectionInterface::class); + $this->_replica = Instance::ensure($config, Connection::className()); return $this->_replica; } } From c48ee8a2caa06b22f1f01c279cc20db6525ef5a0 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 14:08:45 +0300 Subject: [PATCH 12/49] code style --- tests/predis/sentinel/data/ar/Customer.php | 2 +- tests/predis/sentinel/data/ar/Order.php | 8 ++++---- tests/predis/sentinel/data/ar/OrderItem.php | 2 -- tests/predis/standalone/data/ar/Customer.php | 2 +- tests/predis/standalone/data/ar/Order.php | 8 ++++---- tests/predis/standalone/data/ar/OrderItem.php | 2 -- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/predis/sentinel/data/ar/Customer.php b/tests/predis/sentinel/data/ar/Customer.php index 481b837e3..de05c98a1 100644 --- a/tests/predis/sentinel/data/ar/Customer.php +++ b/tests/predis/sentinel/data/ar/Customer.php @@ -31,7 +31,7 @@ class Customer extends ActiveRecord /** * @inheritdoc */ - public function attributes():array + public function attributes(): array { return ['id', 'email', 'name', 'address', 'status', 'profile_id']; } diff --git a/tests/predis/sentinel/data/ar/Order.php b/tests/predis/sentinel/data/ar/Order.php index 0c8478549..a8cb1bffc 100644 --- a/tests/predis/sentinel/data/ar/Order.php +++ b/tests/predis/sentinel/data/ar/Order.php @@ -64,15 +64,15 @@ public function getExpensiveItemsUsingViaWithCallable() return $this->hasMany(Item::className(), ['id' => 'item_id']) ->via('orderItems', function (\yii\redis\ActiveQuery $q) { $q->where(['>=', 'subtotal', 10]); - }); - } + }); + } public function getCheapItemsUsingViaWithCallable() { return $this->hasMany(Item::className(), ['id' => 'item_id']) ->via('orderItems', function (\yii\redis\ActiveQuery $q) { - $q->where(['<', 'subtotal', 10]); - }); + $q->where(['<', 'subtotal', 10]); + }); } /** diff --git a/tests/predis/sentinel/data/ar/OrderItem.php b/tests/predis/sentinel/data/ar/OrderItem.php index 051f4671b..c01153c3c 100644 --- a/tests/predis/sentinel/data/ar/OrderItem.php +++ b/tests/predis/sentinel/data/ar/OrderItem.php @@ -48,6 +48,4 @@ public function getItem() } - - } diff --git a/tests/predis/standalone/data/ar/Customer.php b/tests/predis/standalone/data/ar/Customer.php index 6b3796e2a..ec5b62075 100644 --- a/tests/predis/standalone/data/ar/Customer.php +++ b/tests/predis/standalone/data/ar/Customer.php @@ -31,7 +31,7 @@ class Customer extends ActiveRecord /** * @inheritdoc */ - public function attributes():array + public function attributes(): array { return ['id', 'email', 'name', 'address', 'status', 'profile_id']; } diff --git a/tests/predis/standalone/data/ar/Order.php b/tests/predis/standalone/data/ar/Order.php index 6722f5959..f08bfc3be 100644 --- a/tests/predis/standalone/data/ar/Order.php +++ b/tests/predis/standalone/data/ar/Order.php @@ -64,15 +64,15 @@ public function getExpensiveItemsUsingViaWithCallable() return $this->hasMany(Item::className(), ['id' => 'item_id']) ->via('orderItems', function (\yii\redis\ActiveQuery $q) { $q->where(['>=', 'subtotal', 10]); - }); - } + }); + } public function getCheapItemsUsingViaWithCallable() { return $this->hasMany(Item::className(), ['id' => 'item_id']) ->via('orderItems', function (\yii\redis\ActiveQuery $q) { - $q->where(['<', 'subtotal', 10]); - }); + $q->where(['<', 'subtotal', 10]); + }); } /** diff --git a/tests/predis/standalone/data/ar/OrderItem.php b/tests/predis/standalone/data/ar/OrderItem.php index b065f854b..132ba8553 100644 --- a/tests/predis/standalone/data/ar/OrderItem.php +++ b/tests/predis/standalone/data/ar/OrderItem.php @@ -48,6 +48,4 @@ public function getItem() } - - } From e5f0a66b815763137144cd00aa26dade1089391f Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 15:14:56 +0300 Subject: [PATCH 13/49] del --- src/predis/Command/HashGetAllCommand.php | 18 -------------- src/predis/CommandFactory.php | 21 ----------------- tests/predis/cluster/config/config.php | 30 ------------------------ 3 files changed, 69 deletions(-) delete mode 100644 src/predis/Command/HashGetAllCommand.php delete mode 100644 src/predis/CommandFactory.php delete mode 100644 tests/predis/cluster/config/config.php diff --git a/src/predis/Command/HashGetAllCommand.php b/src/predis/Command/HashGetAllCommand.php deleted file mode 100644 index 7ae5e81d0..000000000 --- a/src/predis/Command/HashGetAllCommand.php +++ /dev/null @@ -1,18 +0,0 @@ -commands['HGETALL'] = HashGetAllCommand::class; -// $this->commands['ZRANGE'] = ZrangeCommand::class; - } - -} diff --git a/tests/predis/cluster/config/config.php b/tests/predis/cluster/config/config.php deleted file mode 100644 index c7b26fe3e..000000000 --- a/tests/predis/cluster/config/config.php +++ /dev/null @@ -1,30 +0,0 @@ -ping(); - - -$config = [ - 'databases' => [ - 'redis' => [ -// 'parameters' => ['tcp://redis-cluster:7000', 'tcp://redis-cluster:7001', 'tcp://redis-cluster:7002'], - 'parameters' => [ - 'tcp://redis-node-0:6379', 'tcp://redis-node-1:6379', 'tcp://redis-node-2:6379', - ], - 'options' => [ - 'cluster' => 'redis', - 'parameters' => [ - 'persistent' => true, - 'conn_uid' => 'id_cluster', - ], - ], - ], - ], -]; - -return $config; From 8d709a3d3d07906e07762d04ae15c6d2d75d6dbc Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 15:49:15 +0300 Subject: [PATCH 14/49] comments --- src/predis/PredisConnection.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/predis/PredisConnection.php b/src/predis/PredisConnection.php index 66f3ac522..04994d0f7 100644 --- a/src/predis/PredisConnection.php +++ b/src/predis/PredisConnection.php @@ -41,24 +41,6 @@ * ], * ], * ]; - * // redis-cluster - * 'redis' = [ - * 'class' => PredisConnection::class, - * 'parameters' => [ - * 'tcp://127.0.0.1:5380?timeout=0.100', - * 'tcp://127.0.0.1:5381?timeout=0.100', - * 'tcp://127.0.0.1:5382?timeout=0.100', - * ], - * 'options' => [ - * 'cluster' => 'redis' - * 'parameters' => [ - * 'password' => 'password', - * // @see \Predis\Connection\StreamConnection - * 'persistent' => true, // performs the connection asynchronously - * 'async_connect' => true, //the connection asynchronously - * 'read_write_timeout' => 0.1, // timeout of read / write operations - * ], - * ]; * ``` */ class PredisConnection extends Component implements ConnectionInterface From a7dcd7d7a719d7d49ea7e730ed011024970fce15 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:06:47 +0300 Subject: [PATCH 15/49] docs --- docs/guide-ja/README.md | 1 + docs/guide-ja/predis.md | 71 +++++++++++++++++++++++ docs/guide-ja/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide-ja/topics-predis-session.md | 40 +++++++++++++ docs/guide-pt-BR/README.md | 1 + docs/guide-pt-BR/predis.md | 71 +++++++++++++++++++++++ docs/guide-pt-BR/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide-pt-BR/topics-predis-session.md | 40 +++++++++++++ docs/guide-ru/README.md | 1 + docs/guide-ru/predis.md | 71 +++++++++++++++++++++++ docs/guide-ru/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide-ru/topics-predis-session.md | 40 +++++++++++++ docs/guide-uz/README.md | 1 + docs/guide-uz/predis.md | 71 +++++++++++++++++++++++ docs/guide-uz/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide-uz/topics-predis-session.md | 40 +++++++++++++ docs/guide-zh-CN/README.md | 1 + docs/guide-zh-CN/predis.md | 71 +++++++++++++++++++++++ docs/guide-zh-CN/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide-zh-CN/topics-predis-session.md | 40 +++++++++++++ docs/guide/README.md | 1 + docs/guide/predis.md | 71 +++++++++++++++++++++++ docs/guide/topics-predis-cache.md | 51 ++++++++++++++++ docs/guide/topics-predis-session.md | 40 +++++++++++++ 24 files changed, 978 insertions(+) create mode 100644 docs/guide-ja/predis.md create mode 100644 docs/guide-ja/topics-predis-cache.md create mode 100644 docs/guide-ja/topics-predis-session.md create mode 100644 docs/guide-pt-BR/predis.md create mode 100644 docs/guide-pt-BR/topics-predis-cache.md create mode 100644 docs/guide-pt-BR/topics-predis-session.md create mode 100644 docs/guide-ru/predis.md create mode 100644 docs/guide-ru/topics-predis-cache.md create mode 100644 docs/guide-ru/topics-predis-session.md create mode 100644 docs/guide-uz/predis.md create mode 100644 docs/guide-uz/topics-predis-cache.md create mode 100644 docs/guide-uz/topics-predis-session.md create mode 100644 docs/guide-zh-CN/predis.md create mode 100644 docs/guide-zh-CN/topics-predis-cache.md create mode 100644 docs/guide-zh-CN/topics-predis-session.md create mode 100644 docs/guide/predis.md create mode 100644 docs/guide/topics-predis-cache.md create mode 100644 docs/guide/topics-predis-session.md diff --git a/docs/guide-ja/README.md b/docs/guide-ja/README.md index e842949f0..bea762d9d 100644 --- a/docs/guide-ja/README.md +++ b/docs/guide-ja/README.md @@ -9,6 +9,7 @@ Yii 2 Redis キャッシュ、セッションおよびアクティブレコー -------- * [インストール](installation.md) +* [Поддержка predis](predis.md) 使用方法 -------- diff --git a/docs/guide-ja/predis.md b/docs/guide-ja/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide-ja/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide-ja/topics-predis-cache.md b/docs/guide-ja/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide-ja/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-ja/topics-predis-session.md b/docs/guide-ja/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide-ja/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/docs/guide-pt-BR/README.md b/docs/guide-pt-BR/README.md index 5efdd8e40..19a9b9916 100644 --- a/docs/guide-pt-BR/README.md +++ b/docs/guide-pt-BR/README.md @@ -7,6 +7,7 @@ Iniciando --------------- * [Instalação](installation.md) +* [Поддержка predis](predis.md) Uso ----- diff --git a/docs/guide-pt-BR/predis.md b/docs/guide-pt-BR/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide-pt-BR/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide-pt-BR/topics-predis-cache.md b/docs/guide-pt-BR/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide-pt-BR/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-pt-BR/topics-predis-session.md b/docs/guide-pt-BR/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide-pt-BR/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/docs/guide-ru/README.md b/docs/guide-ru/README.md index 2302d3f68..f8336377f 100644 --- a/docs/guide-ru/README.md +++ b/docs/guide-ru/README.md @@ -9,6 +9,7 @@ Redis Cache, Session и ActiveRecord для Yii 2 --------------- * [Установка](installation.md) +* [Поддержка predis](predis.md) Использование ----- diff --git a/docs/guide-ru/predis.md b/docs/guide-ru/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide-ru/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide-ru/topics-predis-cache.md b/docs/guide-ru/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide-ru/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-ru/topics-predis-session.md b/docs/guide-ru/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide-ru/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/docs/guide-uz/README.md b/docs/guide-uz/README.md index a2592bacc..bd9b79315 100644 --- a/docs/guide-uz/README.md +++ b/docs/guide-uz/README.md @@ -7,6 +7,7 @@ Ishni boshlash --------------- * [O'rnatish](installation.md) +* [Поддержка predis](predis.md) Foydalanish ----- diff --git a/docs/guide-uz/predis.md b/docs/guide-uz/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide-uz/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide-uz/topics-predis-cache.md b/docs/guide-uz/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide-uz/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-uz/topics-predis-session.md b/docs/guide-uz/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide-uz/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md index be87d1f74..995e66417 100644 --- a/docs/guide-zh-CN/README.md +++ b/docs/guide-zh-CN/README.md @@ -10,6 +10,7 @@ Yii 2 Redis 缓存,会话和活动记录 --------------- * [安装](installation.md) +* [Поддержка predis](predis.md) 用法 ----- diff --git a/docs/guide-zh-CN/predis.md b/docs/guide-zh-CN/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide-zh-CN/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide-zh-CN/topics-predis-cache.md b/docs/guide-zh-CN/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide-zh-CN/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-zh-CN/topics-predis-session.md b/docs/guide-zh-CN/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide-zh-CN/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/docs/guide/README.md b/docs/guide/README.md index 245b81dcd..15c54f1a6 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -10,6 +10,7 @@ Getting Started --------------- * [Installation](installation.md) +* [Поддержка predis](predis.md) Usage ----- diff --git a/docs/guide/predis.md b/docs/guide/predis.md new file mode 100644 index 000000000..877823057 --- /dev/null +++ b/docs/guide/predis.md @@ -0,0 +1,71 @@ +Predis для Redis Cache, Session и ActiveRecord +=============================================== +## Конфигурирование приложения + +Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: + +> [!WARNING] +> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* + +### standalone +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` +### sentinel +```php +return [ + //.... + 'components' => [ + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => [ + 'tcp://redis-node-1:26379', + 'tcp://redis-node-2:26379', + 'tcp://redis-node-3:26379', + ], + 'options' => [ + 'parameters' => [ + 'password' => 'secret', // Or NULL + 'database' => 0, + 'persistent' => true, + 'async_connect' => true, + 'read_write_timeout' => 0.1, + ], + ], + ], + ] +]; +``` + +> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. + +Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: + +```php +Yii::$app->redis->set('mykey', 'some value'); +echo Yii::$app->redis->get('mykey'); +``` + +Дополнительно +----------------- + +* [Использование компонента Cache с predis](topics-predis-cache.md) +* [Использование компонента Session с predis](topics-predis-session.md) + diff --git a/docs/guide/topics-predis-cache.md b/docs/guide/topics-predis-cache.md new file mode 100644 index 000000000..f0aa43936 --- /dev/null +++ b/docs/guide/topics-predis-cache.md @@ -0,0 +1,51 @@ +Использование компонента Cache в месте с predis +========================= + +Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide/topics-predis-session.md b/docs/guide/topics-predis-session.md new file mode 100644 index 000000000..76ac68df6 --- /dev/null +++ b/docs/guide/topics-predis-session.md @@ -0,0 +1,40 @@ +Использование компонента Session в месте с predis +=========================== + +Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): + +```php +return [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` From 5f4fef365f428871a44549779da347f6ffd950fd Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:28:43 +0300 Subject: [PATCH 16/49] build --- .github/workflows/build.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81bb8a45f..a17cf33dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,23 +16,24 @@ jobs: - ubuntu-latest php: - - "5.4" - - "5.5" - - "5.6" - - "7.0" - - "7.1" - - "7.2" - - "7.3" - - "7.4" + - "8.1" + - "8.2" + - "8.3" + - "8.4" steps: - name: Checkout uses: actions/checkout@v2 - - name: Start Redis v4 + - name: Start Redis v8 uses: superchargejs/redis-github-action@1.1.0 with: - redis-version: 4 + redis-version: 8 + + - name: Start Redis Sentinel + uses: superchargejs/redis-github-action@1.1.0 + with: + redis-image: bitnami/redis-sentinel:latest - name: Install PHP with extensions uses: shivammathur/setup-php@v2 From 8f2a33b42554345e433bda6f1d8bdbe90ae52a69 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:34:54 +0300 Subject: [PATCH 17/49] build --- .github/workflows/build.yml | 2 +- .github/workflows/ci-redis.yml | 69 ---------------------------------- 2 files changed, 1 insertion(+), 70 deletions(-) delete mode 100644 .github/workflows/ci-redis.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a17cf33dc..c55a5ab68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache Composer dependencies - uses: actions/cache@v2.1.4 + uses: actions/cache@v4.0.0 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/ci-redis.yml b/.github/workflows/ci-redis.yml deleted file mode 100644 index 7517670c8..000000000 --- a/.github/workflows/ci-redis.yml +++ /dev/null @@ -1,69 +0,0 @@ -on: - - pull_request - - push - -name: ci-redis - -jobs: - tests: - name: PHP ${{ matrix.php }}-redis-${{ matrix.redis }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "7.4" - - redis: - - "5" - - "6" - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Start Redis v${{ matrix.redis }} - uses: superchargejs/redis-github-action@1.1.0 - with: - redis-version: ${{ matrix.redis }} - - - name: Install PHP with extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, intl, redis - ini-values: date.timezone='UTC' - coverage: xdebug - tools: composer:v2, pecl - - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache Composer dependencies - uses: actions/cache@v2.1.4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies with Composer - run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader - - - name: Run Redis 5 tests with PhpUnit - if: matrix.redis == '5' - run: vendor/bin/phpunit - - - name: Run Redis 6 tests with PhpUnit and coverage - if: matrix.redis == '6' - run: vendor/bin/phpunit --coverage-clover=coverage.clover - - - name: Code coverage - if: matrix.redis == '6' - run: | - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover From fbadba2675b80c4bf395b6996759b8355258cd6d Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:35:55 +0300 Subject: [PATCH 18/49] build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c55a5ab68..9418e532c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache Composer dependencies - uses: actions/cache@v4.0.0 + uses: actions/cache@v2 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} From 8a389fc0e9ca727d931c7d61d3bbe1b37f16e07e Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:37:13 +0300 Subject: [PATCH 19/49] build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9418e532c..6743a86c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache Composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} From 32966067fd82b05ac414771d3ef8d98694a2dccf Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:52:27 +0300 Subject: [PATCH 20/49] build --- .github/workflows/build.yml | 40 ++++++------------------------------- Makefile | 12 +++++++++++ 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6743a86c1..7a939bfd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,37 +25,9 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Start Redis v8 - uses: superchargejs/redis-github-action@1.1.0 - with: - redis-version: 8 - - - name: Start Redis Sentinel - uses: superchargejs/redis-github-action@1.1.0 - with: - redis-image: bitnami/redis-sentinel:latest - - - name: Install PHP with extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, intl, redis - ini-values: date.timezone='UTC' - tools: composer:v2, pecl - - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache Composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies with Composer - run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader - - - name: Run Redis tests with PhpUnit - run: vendor/bin/phpunit --coverage-clover=coverage.clover + - name: PHP tests for PHP ${{ matrix.php }} + run: | + make build v=${{ matrix.php }} + make test v=${{ matrix.php }} + + diff --git a/Makefile b/Makefile index b6dd0e982..f0e0b7731 100644 --- a/Makefile +++ b/Makefile @@ -35,3 +35,15 @@ clean: docker rm $(shell cat tests/dockerids/redis) rm tests/dockerids/redis +test-sentinel: + make build + PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml build --pull yii2-redis-php + PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml run yii2-redis-php vendor/bin/phpunit --coverage-clover=coverage.clover + make down + +build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1 + PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml up -d --build + +down: ## Stop and remove containers, networks + docker-compose -f tests/docker/docker-compose.yml down + From 631ef1b1855927907adf64490da5627ba7a27e54 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:52:38 +0300 Subject: [PATCH 21/49] build --- tests/docker/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index 078f1808d..0f4ba74e0 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -4,8 +4,8 @@ version: '3.8' services: - PHP: - image: "yiisoftware/yii2-php:8.1-apache" + yii2-redis-php: + image: "yiisoftware/yii2-php:${PHP_VERSION:-8.1}-apache" networks: - yii2-redis volumes: From f635042a7c1f1b6d1e979e6e84ba18635f625519 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:57:33 +0300 Subject: [PATCH 22/49] build --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a939bfd2..7702322f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ jobs: - name: PHP tests for PHP ${{ matrix.php }} run: | + ls -al make build v=${{ matrix.php }} make test v=${{ matrix.php }} From abe55a73af3fdf9972bbdee01343c0e9977731dc Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:58:12 +0300 Subject: [PATCH 23/49] build --- tests/docker/docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index 0f4ba74e0..a318126f7 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -10,6 +10,12 @@ services: - yii2-redis volumes: - ../..:/app # Mount source-code for development +# - ./php.ini:/usr/local/etc/php/conf.d/php.ini # Добавляем кастомный php.ini +# environment: +# PHP_ENABLE_XDEBUG: 1 +# PHP_IDE_CONFIG: "serverName=Docker" +# extra_hosts: +# - "host.docker.internal:host-gateway" redis: image: "redis" From 285e192710de45b8b8bfcb032aae0152a946c01b Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 16:58:25 +0300 Subject: [PATCH 24/49] build --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7702322f1..94ce82834 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,6 @@ jobs: - name: PHP tests for PHP ${{ matrix.php }} run: | ls -al - make build v=${{ matrix.php }} - make test v=${{ matrix.php }} + make test-sentinel v=${{ matrix.php }} From 351fa25dfdb0eca1ca64cf6f196a34a74a0a0858 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 17:02:45 +0300 Subject: [PATCH 25/49] build --- Makefile | 8 ++++---- tests/docker/docker-compose.yml | 1 - tests/docker/php.ini | 11 +++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 tests/docker/php.ini diff --git a/Makefile b/Makefile index f0e0b7731..296ae15c0 100644 --- a/Makefile +++ b/Makefile @@ -37,13 +37,13 @@ clean: test-sentinel: make build - PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml build --pull yii2-redis-php - PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml run yii2-redis-php vendor/bin/phpunit --coverage-clover=coverage.clover + PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml build --pull yii2-redis-php + PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml run yii2-redis-php vendor/bin/phpunit --coverage-clover=coverage.clover make down build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1 - PHP_VERSION=$(filter-out $@,$(v)) docker-compose -f tests/docker/docker-compose.yml up -d --build + PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml up -d --build down: ## Stop and remove containers, networks - docker-compose -f tests/docker/docker-compose.yml down + docker compose -f tests/docker/docker-compose.yml down diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index a318126f7..7b0eccb3c 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3.8' # NOTE: When using docker-compose for testing, make sure you set 'hostname' to 'redis' in tests/data/config.php diff --git a/tests/docker/php.ini b/tests/docker/php.ini new file mode 100644 index 000000000..000363fb8 --- /dev/null +++ b/tests/docker/php.ini @@ -0,0 +1,11 @@ + +xdebug.mode=debug +xdebug.discover_client_host=1 +xdebug.start_with_request=yes + +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.idekey=PHPSTORM +xdebug.scream=1 +xdebug.force_display_errors=1 + From 3f489e3d199bce0bf795583a6f29b7be9d9ae3b6 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 17:09:48 +0300 Subject: [PATCH 26/49] build --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 296ae15c0..7ae831b87 100644 --- a/Makefile +++ b/Makefile @@ -37,9 +37,9 @@ clean: test-sentinel: make build - PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml build --pull yii2-redis-php - PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml run yii2-redis-php vendor/bin/phpunit --coverage-clover=coverage.clover - make down + PHP_VERSION=$(filter-out $@,$(MAKECMDGOALS)) docker compose -f tests/docker/docker-compose.yml build --pull yii2-redis-php + PHP_VERSION=$(filter-out $@,$(MAKECMDGOALS)) docker compose -f tests/docker/docker-compose.yml up -d + PHP_VERSION=$(filter-out $@,$(MAKECMDGOALS)) docker compose -f tests/docker/docker-compose.yml exec yii2-redis-php sh -c "composer update && vendor/bin/phpunit --coverage-clover=coverage.clover" build: ## Build an image from a docker-compose file. Params: {{ v=8.1 }}. Default latest PHP 8.1 PHP_VERSION=$(filter-out $@,$(v)) docker compose -f tests/docker/docker-compose.yml up -d --build From 73a23124bf191839bd038c9b8ea4ef16dc4344bd Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 17:28:32 +0300 Subject: [PATCH 27/49] fix test --- tests/RedisConnectionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 7185328b3..e045c2065 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -102,11 +102,11 @@ public function testSerialize() public function testConnectionTimeout() { $db = $this->getConnection(false); - $db->configSet('timeout', 1); + $db->configSet('timeout', 2); $this->assertTrue($db->ping()); sleep(1); $this->assertTrue($db->ping()); - sleep(2); + sleep(3); if (method_exists($this, 'setExpectedException')) { $this->setExpectedException('\yii\redis\SocketException'); } else { @@ -122,7 +122,7 @@ public function testConnectionTimeoutRetry() $db = $this->getConnection(false); $db->retries = 1; - $db->configSet('timeout', 1); + $db->configSet('timeout', 2); $this->assertCount(3, $logger->messages, 'log of connection and init commands.'); $this->assertTrue($db->ping()); @@ -131,7 +131,7 @@ public function testConnectionTimeoutRetry() $this->assertTrue($db->ping()); $this->assertCount(5, $logger->messages, 'log +1 ping command.'); - sleep(2); + sleep(3); // reconnect should happen here From f8d0a92a5f0585c5b372e91e55d5e6aedc1d556b Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 17:37:51 +0300 Subject: [PATCH 28/49] fix test --- tests/RedisConnectionTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index e045c2065..9920d74fb 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -107,11 +107,7 @@ public function testConnectionTimeout() sleep(1); $this->assertTrue($db->ping()); sleep(3); - if (method_exists($this, 'setExpectedException')) { - $this->setExpectedException('\yii\redis\SocketException'); - } else { - $this->expectException('\yii\redis\SocketException'); - } + $this->expectException('\yii\redis\SocketException'); $this->assertTrue($db->ping()); } From 9cc9efb19d661a764bd3e91eaf8db8c6a1c06518 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 18:34:24 +0300 Subject: [PATCH 29/49] remove test --- tests/RedisConnectionTest.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 9920d74fb..70be03c8b 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -99,18 +99,6 @@ public function testSerialize() $this->assertTrue($db2->ping()); } - public function testConnectionTimeout() - { - $db = $this->getConnection(false); - $db->configSet('timeout', 2); - $this->assertTrue($db->ping()); - sleep(1); - $this->assertTrue($db->ping()); - sleep(3); - $this->expectException('\yii\redis\SocketException'); - $this->assertTrue($db->ping()); - } - public function testConnectionTimeoutRetry() { $logger = new Logger(); @@ -118,7 +106,7 @@ public function testConnectionTimeoutRetry() $db = $this->getConnection(false); $db->retries = 1; - $db->configSet('timeout', 2); + $db->configSet('timeout', 1); $this->assertCount(3, $logger->messages, 'log of connection and init commands.'); $this->assertTrue($db->ping()); @@ -127,7 +115,7 @@ public function testConnectionTimeoutRetry() $this->assertTrue($db->ping()); $this->assertCount(5, $logger->messages, 'log +1 ping command.'); - sleep(3); + sleep(2); // reconnect should happen here From 76fcc888ccf8fa395f2eb2fb452eb9c4fe9801d3 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 18:44:08 +0300 Subject: [PATCH 30/49] fix test --- tests/RedisConnectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 70be03c8b..16fe81e10 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -171,7 +171,7 @@ public function testConnectionTimeoutRetryCount() try { // should try to reconnect 2 times, before finally failing // results in 3 times sending the PING command to redis - sleep(2); + sleep(3); $db->ping(); } catch (SocketException $e) { $exception = true; From 9a318eefde783c9559e029953eb4c2f722b32c6e Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Wed, 2 Jul 2025 18:57:22 +0300 Subject: [PATCH 31/49] returned and fix testConnectionTimeout --- tests/RedisConnectionTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 16fe81e10..0c38cd5b1 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -99,6 +99,30 @@ public function testSerialize() $this->assertTrue($db2->ping()); } + public function testConnectionTimeout() + { + $db = $this->getConnection(false); + $db->configSet('timeout', 1); + $this->assertTrue($db->ping()); + sleep(1); + $this->assertTrue($db->ping()); + + $db->close(); + $db->on(Connection::EVENT_AFTER_OPEN, function() { + // sleep 2 seconds after connect to make every command time out + sleep(2); + }); + + $exception = false; + try { + sleep(3); + $db->ping(); + } catch (SocketException $e) { + $exception = true; + } + $this->assertTrue($exception, 'SocketException should have been thrown.'); + } + public function testConnectionTimeoutRetry() { $logger = new Logger(); From c002b146fe400b8840a046bed4279c6de640770c Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Thu, 3 Jul 2025 13:39:20 +0300 Subject: [PATCH 32/49] comment --- src/predis/Command/CommandDecorator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/predis/Command/CommandDecorator.php b/src/predis/Command/CommandDecorator.php index bbe21de83..ce02603f1 100644 --- a/src/predis/Command/CommandDecorator.php +++ b/src/predis/Command/CommandDecorator.php @@ -20,10 +20,9 @@ public function __construct(CommandInterface $command) */ public function parseResponse($data) { - return $data; // Ваша реализация + return $data; } - // Делегируем все остальные вызовы оригинальной команде public function __call($method, $args) { return call_user_func_array([$this->originalCommand, $method], $args); From a192ce28d0df49439b2b88bec7ff8e2c5c679813 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Thu, 3 Jul 2025 13:43:11 +0300 Subject: [PATCH 33/49] fix --- tests/RedisConnectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 0c38cd5b1..4c07d2b97 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -195,7 +195,7 @@ public function testConnectionTimeoutRetryCount() try { // should try to reconnect 2 times, before finally failing // results in 3 times sending the PING command to redis - sleep(3); + sleep(4); $db->ping(); } catch (SocketException $e) { $exception = true; From 5d0e4a1b6d45789d5e3c47e06afaa9e5c037cae0 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Thu, 3 Jul 2025 14:24:57 +0300 Subject: [PATCH 34/49] fix tests --- tests/predis/sentinel/ActiveRecordTest.php | 2 +- tests/predis/sentinel/RedisCacheTest.php | 5 ++--- tests/predis/standalone/ActiveRecordTest.php | 2 +- tests/predis/standalone/RedisCacheTest.php | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/predis/sentinel/ActiveRecordTest.php b/tests/predis/sentinel/ActiveRecordTest.php index c0c9254da..df839af65 100644 --- a/tests/predis/sentinel/ActiveRecordTest.php +++ b/tests/predis/sentinel/ActiveRecordTest.php @@ -4,6 +4,7 @@ use yii\redis\ActiveQuery; use yii\redis\LuaScriptBuilder; +use yiiunit\extensions\redis\base\ActiveRecordTestTrait; use yiiunit\extensions\redis\predis\sentinel\data\ar\ActiveRecord; use yiiunit\extensions\redis\predis\sentinel\data\ar\Customer; use yiiunit\extensions\redis\predis\sentinel\data\ar\Item; @@ -11,7 +12,6 @@ use yiiunit\extensions\redis\predis\sentinel\data\ar\OrderItem; use yiiunit\extensions\redis\predis\sentinel\data\ar\OrderItemWithNullFK; use yiiunit\extensions\redis\predis\sentinel\data\ar\OrderWithNullFK; -use yiiunit\framework\ar\ActiveRecordTestTrait; /** * @group redis diff --git a/tests/predis/sentinel/RedisCacheTest.php b/tests/predis/sentinel/RedisCacheTest.php index a67a0b49b..17a2cdfc1 100644 --- a/tests/predis/sentinel/RedisCacheTest.php +++ b/tests/predis/sentinel/RedisCacheTest.php @@ -3,9 +3,8 @@ namespace yiiunit\extensions\redis\predis\sentinel; use yii\redis\Cache; -use yii\redis\Connection; use yii\redis\predis\PredisConnection; -use yiiunit\framework\caching\CacheTestCase; +use yiiunit\extensions\redis\base\AbstractCacheTestCase; /** * Class for testing redis cache backend @@ -13,7 +12,7 @@ * @group redis * @group caching */ -class RedisCacheTest extends CacheTestCase +class RedisCacheTest extends AbstractCacheTestCase { private $_cacheInstance; diff --git a/tests/predis/standalone/ActiveRecordTest.php b/tests/predis/standalone/ActiveRecordTest.php index 5070dd609..bc6551a75 100644 --- a/tests/predis/standalone/ActiveRecordTest.php +++ b/tests/predis/standalone/ActiveRecordTest.php @@ -4,6 +4,7 @@ use yii\redis\ActiveQuery; use yii\redis\LuaScriptBuilder; +use yiiunit\extensions\redis\base\ActiveRecordTestTrait; use yiiunit\extensions\redis\predis\standalone\data\ar\ActiveRecord; use yiiunit\extensions\redis\predis\standalone\data\ar\Customer; use yiiunit\extensions\redis\predis\standalone\data\ar\Item; @@ -11,7 +12,6 @@ use yiiunit\extensions\redis\predis\standalone\data\ar\OrderItem; use yiiunit\extensions\redis\predis\standalone\data\ar\OrderItemWithNullFK; use yiiunit\extensions\redis\predis\standalone\data\ar\OrderWithNullFK; -use yiiunit\framework\ar\ActiveRecordTestTrait; /** * @group redis diff --git a/tests/predis/standalone/RedisCacheTest.php b/tests/predis/standalone/RedisCacheTest.php index 522194199..99a76bf21 100644 --- a/tests/predis/standalone/RedisCacheTest.php +++ b/tests/predis/standalone/RedisCacheTest.php @@ -4,7 +4,7 @@ use yii\redis\Cache; use yii\redis\predis\PredisConnection; -use yiiunit\framework\caching\CacheTestCase; +use yiiunit\extensions\redis\base\AbstractCacheTestCase; /** * Class for testing redis cache backend @@ -12,7 +12,7 @@ * @group redis * @group caching */ -class RedisCacheTest extends CacheTestCase +class RedisCacheTest extends AbstractCacheTestCase { private $_cacheInstance; From 582264e13237c6c7b62a59fa1746ace08968fd7f Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 11:27:27 +0300 Subject: [PATCH 35/49] Update docs/guide-ja/README.md Co-authored-by: Alexander Makarov --- docs/guide-ja/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide-ja/README.md b/docs/guide-ja/README.md index bea762d9d..c124acdec 100644 --- a/docs/guide-ja/README.md +++ b/docs/guide-ja/README.md @@ -9,7 +9,7 @@ Yii 2 Redis キャッシュ、セッションおよびアクティブレコー -------- * [インストール](installation.md) -* [Поддержка predis](predis.md) +* [Predisサポート](predis.md) 使用方法 -------- From 649c0109e3628ae9d49b79dacc33b1c843d20732 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 11:27:43 +0300 Subject: [PATCH 36/49] Update docs/guide-ru/topics-predis-cache.md Co-authored-by: Alexander Makarov --- docs/guide-ru/topics-predis-cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide-ru/topics-predis-cache.md b/docs/guide-ru/topics-predis-cache.md index f0aa43936..6b6ef4059 100644 --- a/docs/guide-ru/topics-predis-cache.md +++ b/docs/guide-ru/topics-predis-cache.md @@ -1,4 +1,4 @@ -Использование компонента Cache в месте с predis +Использование компонента Cache вместе с predis ========================= Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: From 952d60ad5724a6f20073ddc10f96e2fb915ee51e Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 11:27:51 +0300 Subject: [PATCH 37/49] Update docs/guide-ru/topics-predis-session.md Co-authored-by: Alexander Makarov --- docs/guide-ru/topics-predis-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide-ru/topics-predis-session.md b/docs/guide-ru/topics-predis-session.md index 76ac68df6..f02d2e5db 100644 --- a/docs/guide-ru/topics-predis-session.md +++ b/docs/guide-ru/topics-predis-session.md @@ -1,4 +1,4 @@ -Использование компонента Session в месте с predis +Использование компонента Session вместе с predis =========================== Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: From aa6743b6841020b98bb50fe2fef74457eb4d4c9b Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 11:31:26 +0300 Subject: [PATCH 38/49] Update docs/guide-ru/predis.md Co-authored-by: Alexander Makarov --- docs/guide-ru/predis.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guide-ru/predis.md b/docs/guide-ru/predis.md index 877823057..24f2e33f6 100644 --- a/docs/guide-ru/predis.md +++ b/docs/guide-ru/predis.md @@ -4,8 +4,7 @@ Predis для Redis Cache, Session и ActiveRecord Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: Класс `yii\redis\predis\PredisConnection` поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* ### standalone ```php From faf8173f650da7e5bf026f7a69bdb96e0b05337c Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 11:32:38 +0300 Subject: [PATCH 39/49] Update docs/guide/README.md Co-authored-by: Alexander Makarov --- docs/guide/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/README.md b/docs/guide/README.md index 15c54f1a6..31b3d517e 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -10,7 +10,7 @@ Getting Started --------------- * [Installation](installation.md) -* [Поддержка predis](predis.md) +* [predis support](predis.md) Usage ----- From 8407d900f4648a999cc327a145657457c8bed511 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 14:15:09 +0300 Subject: [PATCH 40/49] translation of guide --- docs/guide-ja/predis.md | 19 +++++++++---------- docs/guide-ja/topics-predis-cache.md | 14 ++++++++------ docs/guide-ja/topics-predis-session.md | 8 +++++--- docs/guide-pt-BR/README.md | 2 +- docs/guide-pt-BR/predis.md | 19 +++++++++---------- docs/guide-pt-BR/topics-predis-cache.md | 12 ++++++------ docs/guide-pt-BR/topics-predis-session.md | 8 +++++--- docs/guide-ru/topics-predis-cache.md | 4 ++-- docs/guide-uz/README.md | 2 +- docs/guide-uz/predis.md | 19 +++++++++---------- docs/guide-uz/topics-predis-cache.md | 16 ++++++++++------ docs/guide-uz/topics-predis-session.md | 9 ++++++--- docs/guide-zh-CN/README.md | 2 +- docs/guide-zh-CN/predis.md | 19 +++++++++---------- docs/guide-zh-CN/topics-predis-cache.md | 19 +++++-------------- docs/guide-zh-CN/topics-predis-session.md | 8 +++++--- docs/guide/predis.md | 20 +++++++++----------- docs/guide/topics-predis-cache.md | 14 ++++++++------ docs/guide/topics-predis-session.md | 8 +++++--- 19 files changed, 113 insertions(+), 109 deletions(-) diff --git a/docs/guide-ja/predis.md b/docs/guide-ja/predis.md index 877823057..fefae5f0f 100644 --- a/docs/guide-ja/predis.md +++ b/docs/guide-ja/predis.md @@ -1,11 +1,10 @@ -Predis для Redis Cache, Session и ActiveRecord +Yii 2 Redis キャッシュ、セッションおよびアクティブレコード Predis =============================================== -## Конфигурирование приложения +## アプリケーションを構成する -Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: +このエクステンションを使用するためには、アプリケーション構成情報で [[yii\redis\predis\PredisConnection]] クラスを構成する必要があります。 -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: yii\redis\predis\PredisConnection クラスは redis-cluster 接続をサポートしますが、*cache*、*session*、*ActiveRecord*、*mutex* コンポーネント インタフェースのサポートは提供しません。 ### standalone ```php @@ -54,18 +53,18 @@ return [ ]; ``` -> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. +> 接続構成とオプションの詳細については、predis のドキュメントを参照してください。 -Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: +これで、`redis` アプリケーション・コンポーネントによって、redis ストレージに対する基本的なアクセスが提供されるようになります。 ```php Yii::$app->redis->set('mykey', 'some value'); echo Yii::$app->redis->get('mykey'); ``` -Дополнительно +追加のトピック ----------------- -* [Использование компонента Cache с predis](topics-predis-cache.md) -* [Использование компонента Session с predis](topics-predis-session.md) +* [predisでキャッシュコンポーネントを使用する](topics-predis-cache.md) +* [Predisでセッションコンポーネントを使用する](topics-predis-session.md) diff --git a/docs/guide-ja/topics-predis-cache.md b/docs/guide-ja/topics-predis-cache.md index f0aa43936..54124bd03 100644 --- a/docs/guide-ja/topics-predis-cache.md +++ b/docs/guide-ja/topics-predis-cache.md @@ -1,7 +1,8 @@ -Использование компонента Cache в месте с predis +キャッシュ・コンポーネントを使用する Predis ========================= -Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: +`Cache` コンポーネントを使用するためには、[predis](predis.md) の節で説明した接続の構成に加えて、 +`cache` コンポーネントを [[yii\redis\Cache]] として構成する必要があります。 ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): +redis をキャッシュとしてのみ使用する場合、すなわち、redis のアクティブレコードやセッションを使用しない場合は、接続のパラメータをキャッシュ・コンポーネントの中で構成しても構いません +(この場合、接続のアプリケーション・コンポーネントを構成する必要はありません)。 ```php return [ @@ -39,8 +41,8 @@ return [ ]; ``` -Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +このキャッシュは [[yii\caching\CacheInterface]] の全てのメソッドを提供します。インタフェイスに含まれていない redis 固有のメソッドにアクセスしたい場合は、 +[[yii\redis\ConnectionInterface]] のインスタンスである [[yii\redis\Cache::$redis]] を通じてアクセスすることが出来ます。 ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -48,4 +50,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. +利用可能なメソッドの一覧は [[yii\redis\predis\PredisConnection]] を参照して下さい。 diff --git a/docs/guide-ja/topics-predis-session.md b/docs/guide-ja/topics-predis-session.md index 76ac68df6..0f35fb02b 100644 --- a/docs/guide-ja/topics-predis-session.md +++ b/docs/guide-ja/topics-predis-session.md @@ -1,7 +1,8 @@ -Использование компонента Session в месте с predis +セッション・コンポーネントを使用する Predis =========================== -Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: +`Session` コンポーネントを使用するためには、[predis](predis.md) の節で説明した接続の構成に加えて、 +`session` コンポーネントを [[yii\redis\Session]] として構成する必要があります。 ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): +redis をセッションとしてのみ使用する場合、すなわち、redis のアクティブレコードやキャッシュは使わない場合は、接続のパラメータをセッション・コンポーネントの中で構成しても構いません +(この場合、接続のアプリケーション・コンポーネントを構成する必要はありません)。 ```php return [ diff --git a/docs/guide-pt-BR/README.md b/docs/guide-pt-BR/README.md index 19a9b9916..9a23b7775 100644 --- a/docs/guide-pt-BR/README.md +++ b/docs/guide-pt-BR/README.md @@ -7,7 +7,7 @@ Iniciando --------------- * [Instalação](installation.md) -* [Поддержка predis](predis.md) +* [Suporte predis](predis.md) Uso ----- diff --git a/docs/guide-pt-BR/predis.md b/docs/guide-pt-BR/predis.md index 877823057..ecab38cf2 100644 --- a/docs/guide-pt-BR/predis.md +++ b/docs/guide-pt-BR/predis.md @@ -1,11 +1,10 @@ -Predis для Redis Cache, Session и ActiveRecord +Predis para Redis Cache, Sessão e ActiveRecord para Yii 2 =============================================== -## Конфигурирование приложения +## Configurando a aplicação -Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: +Para usar essa extensão, você precisa parametrizar a classe [[yii\redis\predis\PredisConnection]] na configuração da aplicação: -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: A classe yii\redis\predis\PredisConnection suporta conexão redis-cluster, mas não fornece suporte para as interfaces de componentes *cache*, *session*, *ActiveRecord*, *mutex*. ### standalone ```php @@ -54,18 +53,18 @@ return [ ]; ``` -> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. +> Mais informações sobre configuração e opções de conexão podem ser encontradas na documentação do predis. -Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: +Isto fornece o acesso básico ao armazenamento de redis através do componente de aplicação `redis`: ```php Yii::$app->redis->set('mykey', 'some value'); echo Yii::$app->redis->get('mykey'); ``` -Дополнительно +Additional topics ----------------- -* [Использование компонента Cache с predis](topics-predis-cache.md) -* [Использование компонента Session с predis](topics-predis-session.md) +* [Usando o componente Cache com predis](topics-predis-cache.md) +* [Usando o componente Session com predis](topics-predis-session.md) diff --git a/docs/guide-pt-BR/topics-predis-cache.md b/docs/guide-pt-BR/topics-predis-cache.md index f0aa43936..7dc34f46a 100644 --- a/docs/guide-pt-BR/topics-predis-cache.md +++ b/docs/guide-pt-BR/topics-predis-cache.md @@ -1,7 +1,8 @@ -Использование компонента Cache в месте с predis +Usando o componente Cache com predis ========================= -Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: +Para usar o componente `Cache`, além de configurar a conexão conforme descrito na seção [predis](predis.md), +você também tem que configurar o componente `cache` para ser [[yii\redis\Cache]]: ```php return [ @@ -20,7 +21,7 @@ return [ ]; ``` -Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): +Se você usa apenas o cache de redis (ou seja, não está usando seu ActiveRecord ou Session), você também pode configurar os parâmetros da conexão dentro do componente de cache (nenhum componente de aplicativo de conexão precisa ser configurado neste caso): ```php return [ @@ -39,8 +40,7 @@ return [ ]; ``` -Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +O cache fornece todos os métodos do [[yii\caching\CacheInterface]]. Se você quiser acessar os métodos específicos do redis que não são incluído na interface, você pode usá-los via [[yii\redis\Cache::$redis]], que é uma instância de [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -48,4 +48,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. +Veja [[yii\redis\predis\PredisConnection]] para a lista completa de métodos disponíveis. diff --git a/docs/guide-pt-BR/topics-predis-session.md b/docs/guide-pt-BR/topics-predis-session.md index 76ac68df6..5d44fe7ab 100644 --- a/docs/guide-pt-BR/topics-predis-session.md +++ b/docs/guide-pt-BR/topics-predis-session.md @@ -1,7 +1,8 @@ -Использование компонента Session в месте с predis +Usando o componente Session com predis =========================== -Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: +Para usar o componente `Session`, além de configurar a conexão conforme descrito na seção [predis](predis.md), +você também tem que configurar o componente `session` para ser [[yii\redis\Session]]: ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): +Se você usar somente a sessão de redis (ou seja, não usar seu ActiveRecord ou Cache), você também pode configurar os parâmetros da conexão dentro do +componente de sessão (nenhum componente de aplicativo de conexão precisa ser configurado neste caso): ```php return [ diff --git a/docs/guide-ru/topics-predis-cache.md b/docs/guide-ru/topics-predis-cache.md index 6b6ef4059..5b2477719 100644 --- a/docs/guide-ru/topics-predis-cache.md +++ b/docs/guide-ru/topics-predis-cache.md @@ -40,7 +40,7 @@ return [ ``` Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -48,4 +48,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. +Смотри [[yii\redis\predis\PredisConnection]] для получения полного списка доступных методов. diff --git a/docs/guide-uz/README.md b/docs/guide-uz/README.md index bd9b79315..048731aea 100644 --- a/docs/guide-uz/README.md +++ b/docs/guide-uz/README.md @@ -7,7 +7,7 @@ Ishni boshlash --------------- * [O'rnatish](installation.md) -* [Поддержка predis](predis.md) +* [predis qo'llab-quvvatlash](predis.md) Foydalanish ----- diff --git a/docs/guide-uz/predis.md b/docs/guide-uz/predis.md index 877823057..060b50d22 100644 --- a/docs/guide-uz/predis.md +++ b/docs/guide-uz/predis.md @@ -1,11 +1,10 @@ -Predis для Redis Cache, Session и ActiveRecord +Redis keshi, sessiya va ActiveRecord uchun Predis =============================================== -## Конфигурирование приложения +## Sozlash -Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: +Kengaytmadan foydalanish uchun Yii2 sozlamalaridan `[[yii\redis\predis\PredisConnection]]` sinfini quyidagicha sozlashingiz kerak -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: Yii\redis\predis\PredisConnection klassi redis-klaster ulanishini qo‘llab-quvvatlaydi, lekin *kesh*, *sessiya*, *ActiveRecord*, *mutex* komponent interfeyslarini qo‘llab-quvvatlamaydi. ### standalone ```php @@ -54,18 +53,18 @@ return [ ]; ``` -> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. +> Ulanish konfiguratsiyasi va opsiyalari haqida batafsil ma’lumotni predis hujjatlarida topishingiz mumkin. -Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: +Quyidagi kodlar orqali redis'ga ma'lumot kiritish va o'qishning eng oddiy holatini ko'rish mumkin ```php Yii::$app->redis->set('mykey', 'some value'); echo Yii::$app->redis->get('mykey'); ``` -Дополнительно +Qo'shimcha ----------------- -* [Использование компонента Cache с predis](topics-predis-cache.md) -* [Использование компонента Session с predis](topics-predis-session.md) +* [Kesh komponentidan predis bilan foydalanish](topics-predis-cache.md) +* [Session komponentidan predis bilan foydalanish](topics-predis-session.md) diff --git a/docs/guide-uz/topics-predis-cache.md b/docs/guide-uz/topics-predis-cache.md index f0aa43936..58128a2ce 100644 --- a/docs/guide-uz/topics-predis-cache.md +++ b/docs/guide-uz/topics-predis-cache.md @@ -1,7 +1,8 @@ -Использование компонента Cache в месте с predis +Kesh komponentidan predis bilan foydalanish ========================= -Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: +Redis'dan keshda foydalanish uchun [predis](predis.md) bo'limida tavsiflanganidek sozlashdan tashqari, +[[yii\redis\Cache]] sinfi ham sozlashingiz kerak: ```php return [ @@ -20,7 +21,9 @@ return [ ]; ``` -Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): +Agar siz faqat redis keshidan foydalansangiz (ya'ni, ActiveRecord yoki Sessiya uchun foydalanmasangiz), +kesh komponentini o'zida ulanish sozlamalarini ham kiritishingiz mumkin +(bu holda [predis](predis.md) bo'limidagi sozlashni bajarish shart emas): ```php return [ @@ -39,8 +42,9 @@ return [ ]; ``` -Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +Kesh `[[yii\caching\CacheInterface]]` interfeysidagi barcha metodlardan foydalanish imkonini beradi. +Interfeysga kiritilmagan Redis maxsus metodlaridan foydalanmoqchi bo'lsangiz, [[yii\redis\Cache::$redis]] orqali foydalanishingiz mumkin, +bu [[yii\redis\ConnectionInterface]] holatidagi namuna: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -48,4 +52,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. +Boshqa metodlarni ko'rish uchun `[[yii\redis\predis\PredisConnection]]` sinfiga qarang. diff --git a/docs/guide-uz/topics-predis-session.md b/docs/guide-uz/topics-predis-session.md index 76ac68df6..695d14db0 100644 --- a/docs/guide-uz/topics-predis-session.md +++ b/docs/guide-uz/topics-predis-session.md @@ -1,7 +1,8 @@ -Использование компонента Session в месте с predis +Session komponentidan predis bilan foydalanish =========================== -Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: +Redis'dan sessiyada foydalanish uchun [predis](predis.md) bo'limida tavsiflanganidek sozlashdan tashqari, +[[yii\redis\Session]] sinfi ham sozlashingiz kerak: ```php return [ @@ -20,7 +21,9 @@ return [ ]; ``` -Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): +Agar siz faqat redis sessiyaidan foydalansangiz (ya'ni, ActiveRecord yoki Kesh uchun foydalanmasangiz), +sessiya komponentini o'zida ulanish sozlamalarini ham kiritishingiz mumkin +(bu holda [predis](predis.md) bo'limidagi sozlashni bajarish shart emas): ```php return [ diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md index 995e66417..cad6f9f83 100644 --- a/docs/guide-zh-CN/README.md +++ b/docs/guide-zh-CN/README.md @@ -10,7 +10,7 @@ Yii 2 Redis 缓存,会话和活动记录 --------------- * [安装](installation.md) -* [Поддержка predis](predis.md) +* [支持“predis”](predis.md) 用法 ----- diff --git a/docs/guide-zh-CN/predis.md b/docs/guide-zh-CN/predis.md index 877823057..12c1f29ae 100644 --- a/docs/guide-zh-CN/predis.md +++ b/docs/guide-zh-CN/predis.md @@ -1,11 +1,10 @@ -Predis для Redis Cache, Session и ActiveRecord +Redis Cache、Session 和 ActiveRecord 的“Predis” =============================================== -## Конфигурирование приложения +## 配置应用程序 -Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: +使用此扩展时,需要在你的应用程序配置中配置 [[yii\redis\predis\PredisConnection]] 类: -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: yii\redis\predis\PredisConnection 类支持 redis-cluster 连接,但是不提供对 *cache*、*session*、*​​ActiveRecord*、*mutex* 组件接口的支持。 ### standalone ```php @@ -54,18 +53,18 @@ return [ ]; ``` -> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. +> 有关连接配置和选项的更多信息,请参阅 predis 文档。 -Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: +这通过“redis”应用程序组件提供了对redis存储的基本访问: ```php Yii::$app->redis->set('mykey', 'some value'); echo Yii::$app->redis->get('mykey'); ``` -Дополнительно +附加主题 ----------------- -* [Использование компонента Cache с predis](topics-predis-cache.md) -* [Использование компонента Session с predis](topics-predis-session.md) +* [使用 Predis 的 Cache 组件](topics-predis-cache.md) +* [使用带有 predis 的 Session 组件](topics-predis-session.md) diff --git a/docs/guide-zh-CN/topics-predis-cache.md b/docs/guide-zh-CN/topics-predis-cache.md index f0aa43936..d5345aad4 100644 --- a/docs/guide-zh-CN/topics-predis-cache.md +++ b/docs/guide-zh-CN/topics-predis-cache.md @@ -1,7 +1,8 @@ -Использование компонента Cache в месте с predis +使用 Predis 的 Cache 组件 ========================= -Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: +为了使用 `Cache` 组件,如 [predis](predis.md) 章节中所描述的,除了配置连接, +你也需要配置 [[yii\redis\Cache]] 中的 `cache` 组件: ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): +如果你只使用 redis 缓存(即,不使用它的活动记录或者会话),您还可以配置缓存组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): ```php return [ @@ -38,14 +40,3 @@ return [ ] ]; ``` - -Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: - -```php -Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); -Yii::$app->cache->redis->hget('mykey', 'somefield'); -... -``` - -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-zh-CN/topics-predis-session.md b/docs/guide-zh-CN/topics-predis-session.md index 76ac68df6..d15bce17f 100644 --- a/docs/guide-zh-CN/topics-predis-session.md +++ b/docs/guide-zh-CN/topics-predis-session.md @@ -1,7 +1,8 @@ -Использование компонента Session в месте с predis +将 Session 组件与 predis 结合使用 =========================== -Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: +为了使用 `Session` 组件,如 [predis](predis.md) 章节中所描述的,除了配置连接, +你也需要配置 [[yii\redis\Session]] 中的 `session` 组件: ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): +如果你只使用 redis 会话(即,不使用它的活动记录或者缓存),您还可以配置会话组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): ```php return [ diff --git a/docs/guide/predis.md b/docs/guide/predis.md index 877823057..a0e048df5 100644 --- a/docs/guide/predis.md +++ b/docs/guide/predis.md @@ -1,11 +1,10 @@ -Predis для Redis Cache, Session и ActiveRecord +Predis for Redis Cache, Session и ActiveRecord =============================================== -## Конфигурирование приложения +## Configuring application -Чтобы использовать это расширение, вам необходимо настроить класс [[yii\redis\predis\PredisConnection]] в конфигурации вашего приложения: +To use this extension, you have to configure the [[yii\redis\predis\PredisConnection]] class in your application configuration: -> [!WARNING] -> Класс yii\redis\predis\PredisConnection поддерживает подключение redis-cluster, но не даёт поддержки интерфейсов компонентов *cache*, *session*, *ActiveRecord*, *mutex* +> Warning: The yii\redis\predis\PredisConnection class supports redis-cluster connection, but does not provide support for the *cache*, *session*, *ActiveRecord*, *mutex* component interfaces. ### standalone ```php @@ -54,18 +53,17 @@ return [ ]; ``` -> Больше информации можно о конфигурации подключения и опциях можно получить в документации predis. +> More detailed information about the configuration and connection parameters can be found in the predis documentation. -Это обеспечивает базовый доступ к redis-хранилищу через компонент приложения `redis`: +This provides the basic access to redis storage via the `redis` application component: ```php Yii::$app->redis->set('mykey', 'some value'); echo Yii::$app->redis->get('mykey'); ``` -Дополнительно +Additional topics ----------------- -* [Использование компонента Cache с predis](topics-predis-cache.md) -* [Использование компонента Session с predis](topics-predis-session.md) - +* [Using the Cache Component with Predis](topics-predis-cache.md) +* [Using the Session Component with Predis](topics-predis-session.md) diff --git a/docs/guide/topics-predis-cache.md b/docs/guide/topics-predis-cache.md index f0aa43936..19a5a17e0 100644 --- a/docs/guide/topics-predis-cache.md +++ b/docs/guide/topics-predis-cache.md @@ -1,7 +1,8 @@ -Использование компонента Cache в месте с predis +Using the Cache component with predis ========================= -Чтобы использовать компонент `Cache`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `cache` как [[yii\redis\Cache]]: +To use the `Cache` component, in addition to configuring the connection as described in the [predis](predis.md) section, +you also have to configure the `cache` component to be [[yii\redis\Cache]]: ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только кеш redis (т.е. не используете его ActiveRecord или Session), вы также можете настроить параметры соединения в пределах кеш-компонента (в этом случае необходимо настроить конфигурационный компонент подключения): +If you only use the redis cache (i.e., not using its ActiveRecord or Session), you can also configure the parameters of the connection within the +cache component (no connection application component needs to be configured in this case): ```php return [ @@ -39,8 +41,8 @@ return [ ]; ``` -Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +The cache provides all methods of the [[yii\caching\CacheInterface]]. If you want to access redis specific methods that are not +included in the interface, you can use them via [[yii\redis\Cache::$redis]], which is an instance of [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -48,4 +50,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. +See [[yii\redis\predis\PredisConnection]] for a full list of available methods. diff --git a/docs/guide/topics-predis-session.md b/docs/guide/topics-predis-session.md index 76ac68df6..ff39f8345 100644 --- a/docs/guide/topics-predis-session.md +++ b/docs/guide/topics-predis-session.md @@ -1,7 +1,8 @@ -Использование компонента Session в месте с predis +Using the Session component with predis =========================== -Чтобы использовать компонент `Session`, в дополнение к настройке соединения, как описано в разделе [predis](predis.md), вам также нужно настроить компонент `session` как [[yii\redis\Session]]: +To use the `Session` component, in addition to configuring the connection as described in the [predis](predis.md) section, +you also have to configure the `session` component to be [[yii\redis\Session]]: ```php return [ @@ -20,7 +21,8 @@ return [ ]; ``` -Если вы используете только redis сессии (т.е. не используете его ActiveRecord или Cache), вы также можете настроить параметры соединения в компоненте сеанса (в этом случае не нужно настраивать компонент приложения подключения): +If you only use redis session (i.e., not using its ActiveRecord or Cache), you can also configure the parameters of the connection within the +session component (no connection application component needs to be configured in this case): ```php return [ From 9fb396221dd4df75de689f5e940e06dbbfa16638 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 14:15:28 +0300 Subject: [PATCH 41/49] upd guide --- docs/guide-ja/topics-cache.md | 2 +- docs/guide-pt-BR/topics-cache.md | 2 +- docs/guide-ru/topics-cache.md | 4 ++-- docs/guide-uz/topics-cache.md | 2 +- docs/guide/topics-cache.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guide-ja/topics-cache.md b/docs/guide-ja/topics-cache.md index 713e4a70f..8384e62db 100644 --- a/docs/guide-ja/topics-cache.md +++ b/docs/guide-ja/topics-cache.md @@ -37,7 +37,7 @@ return [ ``` このキャッシュは [[yii\caching\CacheInterface]] の全てのメソッドを提供します。インタフェイスに含まれていない redis 固有のメソッドにアクセスしたい場合は、 -[[yii\redis\Connection]] のインスタンスである [[yii\redis\Cache::$redis]] を通じてアクセスすることが出来ます。 +[[yii\redis\ConnectionInterface]] のインスタンスである [[yii\redis\Cache::$redis]] を通じてアクセスすることが出来ます。 ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); diff --git a/docs/guide-pt-BR/topics-cache.md b/docs/guide-pt-BR/topics-cache.md index 25b9503d2..b33e4cb77 100644 --- a/docs/guide-pt-BR/topics-cache.md +++ b/docs/guide-pt-BR/topics-cache.md @@ -35,7 +35,7 @@ return [ ]; ``` -O cache fornece todos os métodos do [[yii\caching\CacheInterface]]. Se você quiser acessar os métodos específicos do redis que não são incluído na interface, você pode usá-los via [[yii\redis\Cache::$redis]], que é uma instância de [[yii\redis\Connection]]: +O cache fornece todos os métodos do [[yii\caching\CacheInterface]]. Se você quiser acessar os métodos específicos do redis que não são incluído na interface, você pode usá-los via [[yii\redis\Cache::$redis]], que é uma instância de [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); diff --git a/docs/guide-ru/topics-cache.md b/docs/guide-ru/topics-cache.md index 74dacb0ea..547d8613f 100644 --- a/docs/guide-ru/topics-cache.md +++ b/docs/guide-ru/topics-cache.md @@ -35,7 +35,7 @@ return [ ``` Кэш предоставляет все методы [[yii\caching\CacheInterface]]. Если вы хотите получить доступ к определенным redis методам, которые не присутствуют -в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\Connection]]: +в интерфейсе, вы можете использовать их через [[yii\redis\Cache::$redis]], который является экземпляром [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); @@ -43,4 +43,4 @@ Yii::$app->cache->redis->hget('mykey', 'somefield'); ... ``` -Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. \ No newline at end of file +Смотри [[yii\redis\Connection]] для получения полного списка доступных методов. diff --git a/docs/guide-uz/topics-cache.md b/docs/guide-uz/topics-cache.md index f2b87c396..4e59738aa 100644 --- a/docs/guide-uz/topics-cache.md +++ b/docs/guide-uz/topics-cache.md @@ -39,7 +39,7 @@ return [ Kesh `[[yii\caching\CacheInterface]]` interfeysidagi barcha metodlardan foydalanish imkonini beradi. Interfeysga kiritilmagan Redis maxsus metodlaridan foydalanmoqchi bo'lsangiz, [[yii\redis\Cache::$redis]] orqali foydalanishingiz mumkin, -bu [[yii\redis\Connection]] holatidagi namuna: +bu [[yii\redis\ConnectionInterface]] holatidagi namuna: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); diff --git a/docs/guide/topics-cache.md b/docs/guide/topics-cache.md index c90610471..0aaa9569d 100644 --- a/docs/guide/topics-cache.md +++ b/docs/guide/topics-cache.md @@ -37,7 +37,7 @@ return [ ``` The cache provides all methods of the [[yii\caching\CacheInterface]]. If you want to access redis specific methods that are not -included in the interface, you can use them via [[yii\redis\Cache::$redis]], which is an instance of [[yii\redis\Connection]]: +included in the interface, you can use them via [[yii\redis\Cache::$redis]], which is an instance of [[yii\redis\ConnectionInterface]]: ```php Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); From 3b22ac8c6823f2e6437d386a3b1191c5659de2f9 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 14:16:23 +0300 Subject: [PATCH 42/49] remove php.ini --- tests/docker/docker-compose.yml | 6 ------ tests/docker/php.ini | 11 ----------- 2 files changed, 17 deletions(-) delete mode 100644 tests/docker/php.ini diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index 7b0eccb3c..089c29152 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -9,12 +9,6 @@ services: - yii2-redis volumes: - ../..:/app # Mount source-code for development -# - ./php.ini:/usr/local/etc/php/conf.d/php.ini # Добавляем кастомный php.ini -# environment: -# PHP_ENABLE_XDEBUG: 1 -# PHP_IDE_CONFIG: "serverName=Docker" -# extra_hosts: -# - "host.docker.internal:host-gateway" redis: image: "redis" diff --git a/tests/docker/php.ini b/tests/docker/php.ini deleted file mode 100644 index 000363fb8..000000000 --- a/tests/docker/php.ini +++ /dev/null @@ -1,11 +0,0 @@ - -xdebug.mode=debug -xdebug.discover_client_host=1 -xdebug.start_with_request=yes - -xdebug.client_host=host.docker.internal -xdebug.client_port=9003 -xdebug.idekey=PHPSTORM -xdebug.scream=1 -xdebug.force_display_errors=1 - From 0745170370bf4b32b8f87751c272aaa4621151b0 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 14:20:10 +0300 Subject: [PATCH 43/49] upd tests --- tests/predis/sentinel/RedisCacheTest.php | 3 --- tests/predis/sentinel/config/config.php | 2 -- tests/predis/standalone/RedisCacheTest.php | 3 --- tests/predis/standalone/config/config.php | 2 -- 4 files changed, 10 deletions(-) diff --git a/tests/predis/sentinel/RedisCacheTest.php b/tests/predis/sentinel/RedisCacheTest.php index 17a2cdfc1..d3fe98945 100644 --- a/tests/predis/sentinel/RedisCacheTest.php +++ b/tests/predis/sentinel/RedisCacheTest.php @@ -27,9 +27,6 @@ protected function getCacheInstance() $this->markTestSkipped('No redis server connection configured.'); } $connection = new PredisConnection($params); -// if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { -// $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); -// } $this->mockApplication(['components' => ['redis' => $connection]]); diff --git a/tests/predis/sentinel/config/config.php b/tests/predis/sentinel/config/config.php index 6805e0480..afe4481c1 100644 --- a/tests/predis/sentinel/config/config.php +++ b/tests/predis/sentinel/config/config.php @@ -1,8 +1,6 @@ markTestSkipped('No redis server connection configured.'); } $connection = new PredisConnection($params); -// if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) { -// $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription); -// } $this->mockApplication(['components' => ['redis' => $connection]]); diff --git a/tests/predis/standalone/config/config.php b/tests/predis/standalone/config/config.php index e940c668d..d0f619566 100644 --- a/tests/predis/standalone/config/config.php +++ b/tests/predis/standalone/config/config.php @@ -1,8 +1,6 @@ Date: Tue, 8 Jul 2025 14:45:02 +0300 Subject: [PATCH 44/49] upd CommandDecorator --- src/predis/Command/CommandDecorator.php | 28 ++++++++++++++++++------- src/predis/PredisConnection.php | 1 + 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/predis/Command/CommandDecorator.php b/src/predis/Command/CommandDecorator.php index ce02603f1..20818f059 100644 --- a/src/predis/Command/CommandDecorator.php +++ b/src/predis/Command/CommandDecorator.php @@ -8,7 +8,7 @@ class CommandDecorator extends COMMAND { - private $originalCommand; + private CommandInterface $originalCommand; public function __construct(CommandInterface $command) { @@ -16,6 +16,7 @@ public function __construct(CommandInterface $command) } /** + * Yii components expect response without changes * @inheritdoc */ public function parseResponse($data) @@ -23,12 +24,25 @@ public function parseResponse($data) return $data; } - public function __call($method, $args) - { - return call_user_func_array([$this->originalCommand, $method], $args); - } + // Calling methods of the original class + + public function __call($method, $args): mixed { return call_user_func_array([$this->originalCommand, $method], $args); } public function getId():string { return $this->originalCommand->getId(); } - public function setArguments(array $arguments):void { $this->originalCommand->setArguments($arguments); } - public function getArguments():array { return $this->originalCommand->getArguments(); } + + public function setArguments(array $arguments): void { $this->originalCommand->setArguments($arguments); } + + public function getArguments(): array { return $this->originalCommand->getArguments(); } + + public function setSlot($slot): void { $this->originalCommand->setSlot($slot); } + + public function getSlot(): ?int { return $this->originalCommand->getSlot(); } + + public function setRawArguments(array $arguments): void { $this->originalCommand->setRawArguments($arguments); } + + public function getArgument($index): mixed { return $this->originalCommand->getArgument($index); } + + public function parseResp3Response($data): mixed { return $this->originalCommand->parseResp3Response($data); } + + public function serializeCommand(): string { return $this->originalCommand->serializeCommand(); } } diff --git a/src/predis/PredisConnection.php b/src/predis/PredisConnection.php index 04994d0f7..88f6d39fe 100644 --- a/src/predis/PredisConnection.php +++ b/src/predis/PredisConnection.php @@ -324,6 +324,7 @@ public function executeCommand($name, $params = []): mixed $command = $this->client->createCommand($name, $params); $response = $this->client->executeCommand(new CommandDecorator($command)); if ($response instanceof Status) { + // ResponseStatus yii expect as bool return (string)$response === 'OK' || (string)$response === 'PONG'; } return $response; From eb4e18a64a16deb5c55e14520ff3e31255c2adf1 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 14:47:08 +0300 Subject: [PATCH 45/49] upd --- src/Cache.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Cache.php b/src/Cache.php index c09905b16..003c0797d 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -371,11 +371,12 @@ protected function flushValues() * defined in this instance. Only used in getValue() and getValues(). * @since 2.0.8 * @return array|string|ConnectionInterface - * @throws \yii\base\InvalidConfigException|\yii\base\NotSupportedException + * @throws \yii\base\InvalidConfigException */ protected function getReplica() { - if ($this->enableReplicas === false) { + // @NOTE Predis uses its own implementation of balancing + if ($this->enableReplicas === false || $this->redis instanceof \yii\redis\Predis\PredisConnection) { return $this->redis; } @@ -387,12 +388,6 @@ protected function getReplica() return $this->_replica = $this->redis; } - if ($this->redis instanceof \yii\redis\Predis\PredisConnection) { - throw new \yii\base\NotSupportedException( - 'predis on supported replicas', - ); - } - $replicas = $this->replicas; shuffle($replicas); $config = array_shift($replicas); From c0ed9d9cdb54bb7bf30bd3ce3b74197b05b960c1 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 15:10:18 +0300 Subject: [PATCH 46/49] upd readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index eca9faaf9..8f2857c25 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,8 @@ return [ ] ]; ``` + +Additional topics +----------------- + +* [predis support](predis.md) From c9a1ed3bb7de018bf47e75ebfb0a645024f2c101 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 15:12:49 +0300 Subject: [PATCH 47/49] upd changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e13ee02..fadc99e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ Yii Framework 2 redis extension Change Log 2.0.21 under development ------------------------ -- no changes in this release. +- New #276: Added support for predis (antonshevelev) +- New #276: Changed default value of yii\redis\Cache::$forceClusterMode to false (antonshevelev) +- New #276: Implemented yii\redis\ConnectionInterface in yii\redis\Connection (antonshevelev) +- New #276: Updated minimum required PHP version to 8.1 (antonshevelev) 2.0.20 June 05, 2025 From 80646e15e987d98eb93f4167687e29bb243115c3 Mon Sep 17 00:00:00 2001 From: Anton Shevelev Date: Tue, 8 Jul 2025 15:36:04 +0300 Subject: [PATCH 48/49] fix test --- tests/RedisConnectionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index f7bf602bb..bace05546 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -16,6 +16,7 @@ class RedisConnectionTest extends TestCase protected function tearDown(): void { $this->getConnection(false)->configSet('timeout', 0); + $this->getConnection()->close(); parent::tearDown(); } From 6070b6ddc99aedafbbd9124713b74f592541aee2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 8 Jul 2025 17:23:00 +0300 Subject: [PATCH 49/49] Update docs/guide/predis.md --- docs/guide/predis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/predis.md b/docs/guide/predis.md index a0e048df5..e58f5a03f 100644 --- a/docs/guide/predis.md +++ b/docs/guide/predis.md @@ -1,4 +1,4 @@ -Predis for Redis Cache, Session и ActiveRecord +Predis for Redis Cache, Session, and ActiveRecord =============================================== ## Configuring application