diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81bb8a45f..69ff597d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,60 +1,48 @@ on: - - pull_request - - push + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' name: build jobs: tests: - name: PHP ${{ matrix.php }}-redis-4 + name: PHP ${{ matrix.php }}-redis-${{ matrix.redis }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false + matrix: os: - ubuntu-latest php: - - "5.4" - - "5.5" - - "5.6" - - "7.0" - - "7.1" - - "7.2" - "7.3" - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + - "8.4" steps: - name: Checkout uses: actions/checkout@v2 - - name: Start Redis v4 - uses: superchargejs/redis-github-action@1.1.0 - with: - redis-version: 4 - - - 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@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 tests with PhpUnit - run: vendor/bin/phpunit --coverage-clover=coverage.clover + - name: PHP tests for PHP ${{ matrix.php }} + run: | + make test-sentinel v=${{ matrix.php }} 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e13ee02..60703df09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ 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) 2.0.20 June 05, 2025 diff --git a/Makefile b/Makefile index b6dd0e982..7ae831b87 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 $@,$(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 + +down: ## Stop and remove containers, networks + docker compose -f tests/docker/docker-compose.yml down + diff --git a/README.md b/README.md index a56d84f59..c4725d47c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Documentation is at [docs/guide/README.md](docs/guide/README.md). Requirements ------------ -At least redis version 2.6.12 is required for all components to work properly. +At least redis version is required for all components to work properly. Installation ------------ @@ -100,3 +100,8 @@ return [ ] ]; ``` + +Additional topics +----------------- + +* [predis support](/docs/guide/predis.md) diff --git a/composer.json b/composer.json index 045be6205..5b10edfd8 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,13 @@ { "name": "yiisoft/yii2-redis", "description": "Redis Cache, Session and ActiveRecord for the Yii framework", + "keywords": [ + "yii2", + "redis", + "active-record", + "cache", + "session" + ], "keywords": ["yii2", "redis", "active-record", "cache", "session"], "type": "yii2-extension", "license": "BSD-3-Clause", @@ -18,11 +25,13 @@ } ], "require": { + "php": "^7.3 || ^8.0", "yiisoft/yii2": "~2.0.39", - "ext-openssl": "*" + "ext-openssl": "*", + "predis/predis": "^v2.3.0|^3.0" }, "require-dev": { - "phpunit/phpunit": "<7", + "phpunit/phpunit": "9.*", "yiisoft/yii2-dev": "~2.0.39" }, "autoload": { diff --git a/docs/guide-ja/README.md b/docs/guide-ja/README.md index e842949f0..c124acdec 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..fefae5f0f --- /dev/null +++ b/docs/guide-ja/predis.md @@ -0,0 +1,70 @@ +Yii 2 Redis キャッシュ、セッションおよびアクティブレコード Predis +=============================================== +## アプリケーションを構成する + +このエクステンションを使用するためには、アプリケーション構成情報で [[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'); +``` + +追加のトピック +----------------- + +* [predisでキャッシュコンポーネントを使用する](topics-predis-cache.md) +* [Predisでセッションコンポーネントを使用する](topics-predis-session.md) + 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-ja/topics-predis-cache.md b/docs/guide-ja/topics-predis-cache.md new file mode 100644 index 000000000..54124bd03 --- /dev/null +++ b/docs/guide-ja/topics-predis-cache.md @@ -0,0 +1,53 @@ +キャッシュ・コンポーネントを使用する 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 をキャッシュとしてのみ使用する場合、すなわち、redis のアクティブレコードやセッションを使用しない場合は、接続のパラメータをキャッシュ・コンポーネントの中で構成しても構いません +(この場合、接続のアプリケーション・コンポーネントを構成する必要はありません)。 + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +このキャッシュは [[yii\caching\CacheInterface]] の全てのメソッドを提供します。インタフェイスに含まれていない redis 固有のメソッドにアクセスしたい場合は、 +[[yii\redis\ConnectionInterface]] のインスタンスである [[yii\redis\Cache::$redis]] を通じてアクセスすることが出来ます。 + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +利用可能なメソッドの一覧は [[yii\redis\predis\PredisConnection]] を参照して下さい。 diff --git a/docs/guide-ja/topics-predis-session.md b/docs/guide-ja/topics-predis-session.md new file mode 100644 index 000000000..0f35fb02b --- /dev/null +++ b/docs/guide-ja/topics-predis-session.md @@ -0,0 +1,42 @@ +セッション・コンポーネントを使用する 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 をセッションとしてのみ使用する場合、すなわち、redis のアクティブレコードやキャッシュは使わない場合は、接続のパラメータをセッション・コンポーネントの中で構成しても構いません +(この場合、接続のアプリケーション・コンポーネントを構成する必要はありません)。 + +```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..9a23b7775 100644 --- a/docs/guide-pt-BR/README.md +++ b/docs/guide-pt-BR/README.md @@ -7,6 +7,7 @@ Iniciando --------------- * [Instalação](installation.md) +* [Suporte 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..ecab38cf2 --- /dev/null +++ b/docs/guide-pt-BR/predis.md @@ -0,0 +1,70 @@ +Predis para Redis Cache, Sessão e ActiveRecord para Yii 2 +=============================================== +## Configurando a aplicação + +Para usar essa extensão, você precisa parametrizar a classe [[yii\redis\predis\PredisConnection]] na configuração da aplicação: + +> 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 +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, + ], + ], + ], + ] +]; +``` + +> Mais informações sobre configuração e opções de conexão podem ser encontradas na documentação do predis. + +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 +----------------- + +* [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-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-pt-BR/topics-predis-cache.md b/docs/guide-pt-BR/topics-predis-cache.md new file mode 100644 index 000000000..7dc34f46a --- /dev/null +++ b/docs/guide-pt-BR/topics-predis-cache.md @@ -0,0 +1,51 @@ +Usando o componente Cache com predis +========================= + +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 [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +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 [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +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'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +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 new file mode 100644 index 000000000..5d44fe7ab --- /dev/null +++ b/docs/guide-pt-BR/topics-predis-session.md @@ -0,0 +1,42 @@ +Usando o componente Session com predis +=========================== + +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 [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +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 [ + //.... + '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..24f2e33f6 --- /dev/null +++ b/docs/guide-ru/predis.md @@ -0,0 +1,70 @@ +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-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-ru/topics-predis-cache.md b/docs/guide-ru/topics-predis-cache.md new file mode 100644 index 000000000..5b2477719 --- /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\ConnectionInterface]]: + +```php +Yii::$app->cache->redis->hset('mykey', 'somefield', 'somevalue'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +Смотри [[yii\redis\predis\PredisConnection]] для получения полного списка доступных методов. diff --git a/docs/guide-ru/topics-predis-session.md b/docs/guide-ru/topics-predis-session.md new file mode 100644 index 000000000..f02d2e5db --- /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..048731aea 100644 --- a/docs/guide-uz/README.md +++ b/docs/guide-uz/README.md @@ -7,6 +7,7 @@ Ishni boshlash --------------- * [O'rnatish](installation.md) +* [predis qo'llab-quvvatlash](predis.md) Foydalanish ----- diff --git a/docs/guide-uz/predis.md b/docs/guide-uz/predis.md new file mode 100644 index 000000000..060b50d22 --- /dev/null +++ b/docs/guide-uz/predis.md @@ -0,0 +1,70 @@ +Redis keshi, sessiya va ActiveRecord uchun Predis +=============================================== +## Sozlash + +Kengaytmadan foydalanish uchun Yii2 sozlamalaridan `[[yii\redis\predis\PredisConnection]]` sinfini quyidagicha sozlashingiz kerak + +> Warning: Yii\redis\predis\PredisConnection klassi redis-klaster ulanishini qo‘llab-quvvatlaydi, lekin *kesh*, *sessiya*, *ActiveRecord*, *mutex* komponent interfeyslarini qo‘llab-quvvatlamaydi. + +### 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, + ], + ], + ], + ] +]; +``` + +> Ulanish konfiguratsiyasi va opsiyalari haqida batafsil ma’lumotni predis hujjatlarida topishingiz mumkin. + +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 +----------------- + +* [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-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-uz/topics-predis-cache.md b/docs/guide-uz/topics-predis-cache.md new file mode 100644 index 000000000..58128a2ce --- /dev/null +++ b/docs/guide-uz/topics-predis-cache.md @@ -0,0 +1,55 @@ +Kesh komponentidan predis bilan foydalanish +========================= + +Redis'dan keshda foydalanish uchun [predis](predis.md) bo'limida tavsiflanganidek sozlashdan tashqari, +[[yii\redis\Cache]] sinfi ham sozlashingiz kerak: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +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 [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +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'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +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 new file mode 100644 index 000000000..695d14db0 --- /dev/null +++ b/docs/guide-uz/topics-predis-session.md @@ -0,0 +1,43 @@ +Session komponentidan predis bilan foydalanish +=========================== + +Redis'dan sessiyada foydalanish uchun [predis](predis.md) bo'limida tavsiflanganidek sozlashdan tashqari, +[[yii\redis\Session]] sinfi ham sozlashingiz kerak: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +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 [ + //.... + '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..cad6f9f83 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..12c1f29ae --- /dev/null +++ b/docs/guide-zh-CN/predis.md @@ -0,0 +1,70 @@ +Redis Cache、Session 和 ActiveRecord 的“Predis” +=============================================== +## 配置应用程序 + +使用此扩展时,需要在你的应用程序配置中配置 [[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'); +``` + +附加主题 +----------------- + +* [使用 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 new file mode 100644 index 000000000..d5345aad4 --- /dev/null +++ b/docs/guide-zh-CN/topics-predis-cache.md @@ -0,0 +1,42 @@ +使用 Predis 的 Cache 组件 +========================= + +为了使用 `Cache` 组件,如 [predis](predis.md) 章节中所描述的,除了配置连接, +你也需要配置 [[yii\redis\Cache]] 中的 `cache` 组件: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +如果你只使用 redis 缓存(即,不使用它的活动记录或者会话),您还可以配置缓存组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): + +```php +return [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` 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..d15bce17f --- /dev/null +++ b/docs/guide-zh-CN/topics-predis-session.md @@ -0,0 +1,42 @@ +将 Session 组件与 predis 结合使用 +=========================== + +为了使用 `Session` 组件,如 [predis](predis.md) 章节中所描述的,除了配置连接, +你也需要配置 [[yii\redis\Session]] 中的 `session` 组件: + +```php +return [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +如果你只使用 redis 会话(即,不使用它的活动记录或者缓存),您还可以配置会话组件内的 +连接参数(在这种情况下,不需要配置连接应用程序的组件): + +```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..31b3d517e 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -10,6 +10,7 @@ Getting Started --------------- * [Installation](installation.md) +* [predis support](predis.md) Usage ----- diff --git a/docs/guide/predis.md b/docs/guide/predis.md new file mode 100644 index 000000000..e58f5a03f --- /dev/null +++ b/docs/guide/predis.md @@ -0,0 +1,69 @@ +Predis for Redis Cache, Session, and ActiveRecord +=============================================== +## Configuring application + +To use this extension, you have to configure the [[yii\redis\predis\PredisConnection]] class in your application configuration: + +> 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 +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, + ], + ], + ], + ] +]; +``` + +> More detailed information about the configuration and connection parameters can be found in the predis documentation. + +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 +----------------- + +* [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-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'); diff --git a/docs/guide/topics-predis-cache.md b/docs/guide/topics-predis-cache.md new file mode 100644 index 000000000..19a5a17e0 --- /dev/null +++ b/docs/guide/topics-predis-cache.md @@ -0,0 +1,53 @@ +Using the Cache component with predis +========================= + +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 [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'cache' => [ + 'class' => 'yii\redis\Cache', + ], + ] +]; +``` + +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 [ + //.... + 'components' => [ + // ... + 'cache' => [ + 'class' => 'yii\redis\Cache', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` + +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'); +Yii::$app->cache->redis->hget('mykey', 'somefield'); +... +``` + +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 new file mode 100644 index 000000000..ff39f8345 --- /dev/null +++ b/docs/guide/topics-predis-session.md @@ -0,0 +1,42 @@ +Using the Session component with predis +=========================== + +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 [ + //.... + 'components' => [ + // ... + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + 'session' => [ + 'class' => 'yii\redis\Session', + ], + ] +]; +``` + +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 [ + //.... + 'components' => [ + // ... + 'session' => [ + 'class' => 'yii\redis\Session', + 'redis' => [ + 'class' => 'yii\redis\predis\PredisConnection', + 'parameters' => 'tcp://redis:6379', + // ... + ], + ], + ] +]; +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 742d0b01b..b7fae8ff1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,18 +1,22 @@ - - - - ./tests - - - - - src/ - - + + + + ./tests + + + + + src + + diff --git a/src/Cache.php b/src/Cache.php index d9a4ad791..003c0797d 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; @@ -101,7 +100,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 @@ -139,7 +138,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) @@ -151,7 +150,7 @@ class Cache extends \yii\caching\Cache public $shareDatabase = false; /** - * @var Connection currently active connection. + * @var ConnectionInterface currently active connection. */ private $_replica; /** @@ -172,7 +171,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,12 +370,13 @@ 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() { - 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; } diff --git a/src/Connection.php b/src/Connection.php index 3d35fd200..c334d4b0d 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,14 +410,14 @@ 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; } $connection = $this->connectionString . ', database=' . $this->database; - \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__); + \Yii::debug('Opening redis DB connection: ' . $connection, __METHOD__); $socket = @stream_socket_client( $this->connectionString, $errorNumber, @@ -668,11 +454,11 @@ 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; - \Yii::trace('Closing DB connection: ' . $connection, __METHOD__); + \Yii::debug('Closing DB connection: ' . $connection, __METHOD__); try { $this->executeCommand('QUIT'); } catch (SocketException $e) { @@ -769,7 +555,7 @@ public function executeCommand($name, $params = []) $command .= '$' . mb_strlen($arg ?? '', '8bit') . "\r\n" . $arg . "\r\n"; } - \Yii::trace("Executing Redis Command: {$name}", __METHOD__); + \Yii::debug("Executing Redis Command: {$name}", __METHOD__); if ($this->retries > 0) { $tries = $this->retries; while ($tries-- > 0) { diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php new file mode 100644 index 000000000..4547d37aa --- /dev/null +++ b/src/ConnectionInterface.php @@ -0,0 +1,234 @@ + + * @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; + + /** + * @param $name + * @param $params + * @return mixed + */ + public function executeCommand($name, $params = []); +} 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/Command/CommandDecorator.php b/src/predis/Command/CommandDecorator.php new file mode 100644 index 000000000..c9379f511 --- /dev/null +++ b/src/predis/Command/CommandDecorator.php @@ -0,0 +1,48 @@ +originalCommand = $command; + } + + /** + * Yii components expect response without changes + * @inheritdoc + */ + public function parseResponse($data) + { + return $data; + } + + // Calling methods of the original class + + 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(); } + + 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) { return $this->originalCommand->getArgument($index); } + + public function parseResp3Response($data) { 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 new file mode 100644 index 000000000..e4d59b383 --- /dev/null +++ b/src/predis/PredisConnection.php @@ -0,0 +1,404 @@ + 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' => '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 + * ], + * ], + * ]; + * ``` + */ +class PredisConnection extends Component implements ConnectionInterface +{ + /** + * @event Event an event that is triggered after a DB connection is established + */ + public 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(): LuaScriptBuilder + { + 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(): void + { + $this->trigger(self::EVENT_AFTER_OPEN); + } + + /** + * @var mixed Connection parameters for one or more servers. + */ + public $parameters; + + /** + * @var mixed Options to configure some behaviours of the client. + */ + public $options = []; + + /** + * @var Client|null redis connection + */ + protected $client; + + /** + * Returns a value indicating whether the DB connection is established. + * + * @return bool whether the DB connection is established + */ + public function getIsActive(): bool + { + if($this->client === null) { + return false; + } + return $this->client->isConnected(); + } + + /** + * @return mixed|ErrorInterface|ResponseInterface + * @throws InvalidConfigException + */ + public function executeCommand($name, $params = []) + { + $this->open(); + + Yii::debug("Executing Redis Command: $name " . implode(' ', $params), __METHOD__); + + $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; + } + + /** + * Establishes a DB connection. + * + * @return void + * @throws InvalidConfigException + */ + public function open(): void + { + if (null !== $this->client) { + return; + } + + if (empty($this->parameters)) { + throw new InvalidConfigException('Connection::parameters cannot be empty'); + } + + Yii::debug('Opening redis DB connection', __METHOD__); + + $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 + { + if($this->client === null) { + return; + } + $this->client->disconnect(); + } + + /** + * Get predis Client + * + * @return Client|null + * @throws InvalidConfigException + */ + public function getClient(): ?Client + { + $this->open(); + + return $this->client; + } + + /** + * 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 + * @throws InvalidConfigException + */ + public function __call($name, $params) + { + $redisCommand = strtoupper(Inflector::camel2words($name, false)); + if (in_array($redisCommand, $this->redisCommands, true)) { + return $this->executeCommand($redisCommand, $params); + } + + return parent::__call($name, $params); + } +} 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..b1192c813 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -4,6 +4,7 @@ use yii\redis\ActiveQuery; use yii\redis\LuaScriptBuilder; +use yiiunit\extensions\redis\base\ActiveRecordTestTrait; use yiiunit\extensions\redis\data\ar\ActiveRecord; use yiiunit\extensions\redis\data\ar\Customer; use yiiunit\extensions\redis\data\ar\OrderItem; @@ -11,7 +12,6 @@ use yiiunit\extensions\redis\data\ar\Item; use yiiunit\extensions\redis\data\ar\OrderItemWithNullFK; use yiiunit\extensions\redis\data\ar\OrderWithNullFK; -use yiiunit\framework\ar\ActiveRecordTestTrait; /** * @group redis @@ -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/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'); diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php index 0cd57277e..29de15041 100644 --- a/tests/RedisConnectionTest.php +++ b/tests/RedisConnectionTest.php @@ -13,16 +13,17 @@ */ class ConnectionTest extends TestCase { - protected function tearDown() + protected function tearDown(): void { $this->getConnection(false)->configSet('timeout', 0); + $this->getConnection()->close(); parent::tearDown(); } /** * test connection to redis and selection of db */ - public function testConnect() + public function testConnect(): void { $db = $this->getConnection(false); $database = $db->database; @@ -47,7 +48,7 @@ public function testConnect() /** * tests whether close cleans up correctly so that a new connect works */ - public function testReConnect() + public function testReConnect(): void { $db = $this->getConnection(false); $db->open(); @@ -63,7 +64,7 @@ public function testReConnect() /** * @return array */ - public function keyValueData() + public function keyValueData(): array { return [ [123], @@ -79,7 +80,7 @@ public function keyValueData() * @dataProvider keyValueData * @param mixed $data */ - public function testStoreGet($data) + public function testStoreGet(mixed $data):void { $db = $this->getConnection(true); @@ -87,7 +88,7 @@ public function testStoreGet($data) $this->assertEquals($data, $db->get('hi')); } - public function testSerialize() + public function testSerialize(): void { $db = $this->getConnection(false); $db->open(); @@ -99,23 +100,36 @@ public function testSerialize() $this->assertTrue($db2->ping()); } - public function testConnectionTimeout() + /** + * @skip Flaky Test fixme + */ + public function testConnectionTimeout(): void { + $this->markTestSkipped('This test is skipped due to flakiness.'); + $db = $this->getConnection(false); $db->configSet('timeout', 1); $this->assertTrue($db->ping()); sleep(1); $this->assertTrue($db->ping()); - sleep(2); - if (method_exists($this, 'setExpectedException')) { - $this->setExpectedException('\yii\redis\SocketException'); - } else { - $this->expectException('\yii\redis\SocketException'); + + $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(4); + $db->ping(); + } catch (SocketException $e) { + $exception = true; } - $this->assertTrue($db->ping()); + $this->assertTrue($exception, 'SocketException should have been thrown.'); } - public function testConnectionTimeoutRetry() + public function testConnectionTimeoutRetry(): void { $logger = new Logger(); Yii::setLogger($logger); @@ -137,16 +151,16 @@ public function testConnectionTimeoutRetry() $this->assertTrue($db->ping()); $this->assertCount(11, $logger->messages, 'log +1 ping command, and reconnection.' - . print_r(array_map(function($s) { return (string) $s; }, ArrayHelper::getColumn($logger->messages, 0)), true)); + . print_r(array_map(static function($s) { return (string) $s; }, ArrayHelper::getColumn($logger->messages, 0)), true)); } - public function testConnectionTimeoutRetryWithFirstFail() + public function testConnectionTimeoutRetryWithFirstFail(): void { $logger = new Logger(); Yii::setLogger($logger); $databases = TestCase::getParam('databases'); - $redis = isset($databases['redis']) ? $databases['redis'] : []; + $redis = $databases['redis'] ?? []; $db = new ConnectionWithErrorEmulator($redis); $db->retries = 3; @@ -163,14 +177,17 @@ public function testConnectionTimeoutRetryWithFirstFail() $this->assertTrue($db->ping()); $this->assertCount(10, $logger->messages, 'log +1 ping command, and two reconnections.' - . print_r(array_map(function($s) { return (string) $s; }, ArrayHelper::getColumn($logger->messages, 0)), true)); + . print_r(array_map(static function($s) { return (string) $s; }, ArrayHelper::getColumn($logger->messages, 0)), true)); } /** * Retry connecting 2 times + * @skip Flaky Test fixme */ - public function testConnectionTimeoutRetryCount() + public function testConnectionTimeoutRetryCount(): void { + $this->markTestSkipped('This test is skipped due to flakiness.'); + $logger = new Logger(); Yii::setLogger($logger); @@ -187,7 +204,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(4); $db->ping(); } catch (SocketException $e) { $exception = true; @@ -200,7 +217,7 @@ public function testConnectionTimeoutRetryCount() /** * https://github.com/yiisoft/yii2/issues/4745 */ - public function testReturnType() + public function testReturnType(): void { $redis = $this->getConnection(); $redis->executeCommand('SET', ['key1', 'val1']); @@ -223,18 +240,18 @@ public function testReturnType() } } - public function testTwoWordCommands() + public function testTwoWordCommands(): void { $redis = $this->getConnection(); - $this->assertTrue(is_array($redis->executeCommand('CONFIG GET', ['port']))); - $this->assertTrue(is_string($redis->clientList())); - $this->assertTrue(is_string($redis->executeCommand('CLIENT LIST'))); + $this->assertIsArray($redis->executeCommand('CONFIG GET', ['port'])); + $this->assertIsString($redis->clientList()); + $this->assertIsString($redis->executeCommand('CLIENT LIST')); } /** * @return array */ - public function zRangeByScoreData() + public function zRangeByScoreData(): array { return [ [ @@ -276,17 +293,17 @@ public function zRangeByScoreData() * @param array $members * @param array $cases */ - public function testZRangeByScore($members, $cases) + public function testZRangeByScore(array $members, array $cases): void { $redis = $this->getConnection(); $set = 'zrangebyscore'; foreach ($members as $member) { - list($name, $score) = $member; + [$name, $score] = $member; $this->assertEquals(1, $redis->zadd($set, $score, $name)); } foreach ($cases as $case) { - list($min, $max, $withScores, $limit, $offset, $count, $expectedRows) = $case; + [$min, $max, $withScores, $limit, $offset, $count, $expectedRows] = $case; if ($withScores !== null && $limit !== null) { $rows = $redis->zrangebyscore($set, $min, $max, $withScores, $limit, $offset, $count); } elseif ($withScores !== null) { @@ -307,7 +324,7 @@ public function testZRangeByScore($members, $cases) /** * @return array */ - public function hmSetData() + public function hmSetData(): array { return [ [ @@ -334,7 +351,7 @@ public function hmSetData() * @param array $params * @param array $pairs */ - public function testHMSet($params, $pairs) + public function testHMSet(array $params, array $pairs): void { $redis = $this->getConnection(); $set = $params[0]; 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/base/AbstractCacheTestCase.php b/tests/base/AbstractCacheTestCase.php new file mode 100644 index 000000000..12f50bb2c --- /dev/null +++ b/tests/base/AbstractCacheTestCase.php @@ -0,0 +1,302 @@ +mockApplication(); + } + + protected function tearDown(): void + { + static::$time = null; + static::$microtime = null; + } + + /** + * @return CacheInterface + */ + public function prepare() + { + $cache = $this->getCacheInstance(); + + $cache->flush(); + $cache->set('string_test', 'string_test'); + $cache->set('number_test', 42); + $cache->set('array_test', ['array_test' => 'array_test']); + $cache['arrayaccess_test'] = new \stdClass(); + + return $cache; + } + + public function testSet() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', ['array_test' => 'array_test'])); + } + + public function testGet() + { + $cache = $this->prepare(); + + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + } + + /** + * @return array testing multiSet with and without expiry + */ + public function multiSetExpiry() + { + return [[0], [2]]; + } + + /** + * @dataProvider multiSetExpiry + * @param int $expiry + */ + public function testMultiset($expiry) + { + $cache = $this->getCacheInstance(); + $cache->flush(); + + $cache->multiSet([ + 'string_test' => 'string_test', + 'number_test' => 42, + 'array_test' => ['array_test' => 'array_test'], + ], $expiry); + + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertEquals(42, $cache->get('number_test')); + + $array = $cache->get('array_test'); + $this->assertArrayHasKey('array_test', $array); + $this->assertEquals('array_test', $array['array_test']); + } + + public function testExists() + { + $cache = $this->prepare(); + + $this->assertTrue($cache->exists('string_test')); + // check whether exists affects the value + $this->assertEquals('string_test', $cache->get('string_test')); + + $this->assertTrue($cache->exists('number_test')); + $this->assertFalse($cache->exists('not_exists')); + } + + public function testArrayAccess() + { + $cache = $this->getCacheInstance(); + + $cache['arrayaccess_test'] = new \stdClass(); + $this->assertInstanceOf('stdClass', $cache['arrayaccess_test']); + } + + public function testGetValueNonExistent() + { + $cache = $this->getCacheInstance(); + + $this->assertFalse($this->invokeMethod($cache, 'getValue', ['non_existent_key'])); + } + + public function testGetNonExistent() + { + $cache = $this->getCacheInstance(); + + $this->assertFalse($cache->get('non_existent_key')); + } + + public function testStoreSpecialValues() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('null_value', null)); + $this->assertNull($cache->get('null_value')); + + $this->assertTrue($cache->set('bool_value', true)); + $this->assertTrue($cache->get('bool_value')); + } + + public function testMultiGet() + { + $cache = $this->prepare(); + + $this->assertEquals(['string_test' => 'string_test', 'number_test' => 42], $cache->multiGet(['string_test', 'number_test'])); + // ensure that order does not matter + $this->assertEquals(['number_test' => 42, 'string_test' => 'string_test'], $cache->multiGet(['number_test', 'string_test'])); + $this->assertSame(['number_test' => 42, 'non_existent_key' => false], $cache->multiGet(['number_test', 'non_existent_key'])); + } + + public function testDefaultTtl() + { + $cache = $this->getCacheInstance(); + + $this->assertSame(0, $cache->defaultDuration); + } + + public function testExpire() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); + usleep(500000); + $this->assertEquals('expire_test', $cache->get('expire_test')); + usleep(2500000); + $this->assertFalse($cache->get('expire_test')); + } + + public function testExpireAdd() + { + $cache = $this->getCacheInstance(); + + $this->assertTrue($cache->add('expire_testa', 'expire_testa', 2)); + usleep(500000); + $this->assertEquals('expire_testa', $cache->get('expire_testa')); + usleep(2500000); + $this->assertFalse($cache->get('expire_testa')); + } + + public function testAdd() + { + $cache = $this->prepare(); + + // should not change existing keys + $this->assertFalse($cache->add('number_test', 13)); + $this->assertEquals(42, $cache->get('number_test')); + + // should store data if it's not there yet + $this->assertFalse($cache->get('add_test')); + $this->assertTrue($cache->add('add_test', 13)); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testMultiAdd() + { + $cache = $this->prepare(); + + $this->assertFalse($cache->get('add_test')); + + $cache->multiAdd([ + 'number_test' => 13, + 'add_test' => 13, + ]); + + $this->assertEquals(42, $cache->get('number_test')); + $this->assertEquals(13, $cache->get('add_test')); + } + + public function testDelete() + { + $cache = $this->prepare(); + + $this->assertEquals(42, $cache->get('number_test')); + $this->assertTrue($cache->delete('number_test')); + $this->assertFalse($cache->get('number_test')); + } + + public function testFlush() + { + $cache = $this->prepare(); + $this->assertTrue($cache->flush()); + $this->assertFalse($cache->get('number_test')); + } + + public function testGetOrSet() + { + $cache = $this->prepare(); + + $expected = $this->getOrSetCallable($cache); + $callable = [$this, 'getOrSetCallable']; + + $this->assertFalse($cache->get('something')); + $this->assertEquals($expected, $cache->getOrSet('something', $callable)); + $this->assertEquals($expected, $cache->get('something')); + } + + public function getOrSetCallable($cache) + { + return get_class($cache); + } + + public function testGetOrSetWithDependencies() + { + $cache = $this->prepare(); + $dependency = new TagDependency(['tags' => 'test']); + + $expected = 'SilverFire'; + $loginClosure = function ($cache) use (&$login) { return 'SilverFire'; }; + $this->assertEquals($expected, $cache->getOrSet('some-login', $loginClosure, null, $dependency)); + + // Call again with another login to make sure that value is cached + $loginClosure = function ($cache) use (&$login) { return 'SamDark'; }; + $this->assertEquals($expected, $cache->getOrSet('some-login', $loginClosure, null, $dependency)); + + $dependency->invalidate($cache, 'test'); + $expected = 'SamDark'; + $this->assertEquals($expected, $cache->getOrSet('some-login', $loginClosure, null, $dependency)); + } +} diff --git a/tests/base/ActiveRecordTestTrait.php b/tests/base/ActiveRecordTestTrait.php new file mode 100644 index 000000000..665d57b6f --- /dev/null +++ b/tests/base/ActiveRecordTestTrait.php @@ -0,0 +1,1313 @@ +getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + // find one + $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); + + // find by expression + $customer = $customerClass::findOne(new Expression('[[id]] = :id', [':id' => 2])); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer = $customerClass::findOne( + new Expression('[[id]] = :id AND [[name]] = :name', [':id' => 2, ':name' => 'user1']) + ); + $this->assertNull($customer); + $customer = $customerClass::findOne(new Expression('[[id]] = :id', [':id' => 5])); + $this->assertNull($customer); + $customer = $customerClass::findOne(new Expression('[[name]] = :name', [':name' => 'user5'])); + $this->assertNull($customer); + + // scope + $this->assertCount(2, $customerClass::find()->active()->all()); + $this->assertEquals(2, $customerClass::find()->active()->count()); + } + + public function testFindAsArray() + { + /* @var $customerClass 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, + 'profile_id' => null, + ], $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 testHasAttribute() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + $customer = new $customerClass(); + $this->assertTrue($customer->hasAttribute('id')); + $this->assertTrue($customer->hasAttribute('email')); + $this->assertFalse($customer->hasAttribute(0)); + $this->assertFalse($customer->hasAttribute(null)); + $this->assertFalse($customer->hasAttribute(42)); + + $customer = $customerClass::findOne(1); + $this->assertTrue($customer->hasAttribute('id')); + $this->assertTrue($customer->hasAttribute('email')); + $this->assertFalse($customer->hasAttribute(0)); + $this->assertFalse($customer->hasAttribute(null)); + $this->assertFalse($customer->hasAttribute(42)); + } + + public function testFindScalar() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + // query scalar + $customerName = $customerClass::find()->where(['id' => 2])->scalar('name'); + $this->assertEquals('user2', $customerName); + $customerName = $customerClass::find()->where(['status' => 2])->scalar('name'); + $this->assertEquals('user3', $customerName); + $customerName = $customerClass::find()->where(['status' => 2])->scalar('noname'); + $this->assertNull($customerName); + $customerId = $customerClass::find()->where(['status' => 2])->scalar('id'); + $this->assertEquals(3, $customerId); + } + + public function testFindColumn() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $this->assertEquals(['user1', 'user2', 'user3'], $customerClass::find()->orderBy(['name' => SORT_ASC])->column('name')); + $this->assertEquals(['user3', 'user2', 'user1'], $customerClass::find()->orderBy(['name' => SORT_DESC])->column('name')); + } + + public function testFindIndexBy() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + // indexBy + $customers = $customerClass::find()->indexBy('name')->orderBy('id')->all(); + $this->assertCount(3, $customers); + $this->assertInstanceOf($customerClass, $customers['user1']); + $this->assertInstanceOf($customerClass, $customers['user2']); + $this->assertInstanceOf($customerClass, $customers['user3']); + + // indexBy callable + $customers = $customerClass::find()->indexBy(function ($customer) { + return $customer->id . '-' . $customer->name; + })->orderBy('id')->all(); + $this->assertCount(3, $customers); + $this->assertInstanceOf($customerClass, $customers['1-user1']); + $this->assertInstanceOf($customerClass, $customers['2-user2']); + $this->assertInstanceOf($customerClass, $customers['3-user3']); + } + + public function testFindIndexByAsArray() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + // indexBy + asArray + $customers = $customerClass::find()->asArray()->indexBy('name')->all(); + $this->assertCount(3, $customers); + $this->assertArrayHasKey('id', $customers['user1']); + $this->assertArrayHasKey('name', $customers['user1']); + $this->assertArrayHasKey('email', $customers['user1']); + $this->assertArrayHasKey('address', $customers['user1']); + $this->assertArrayHasKey('status', $customers['user1']); + $this->assertArrayHasKey('id', $customers['user2']); + $this->assertArrayHasKey('name', $customers['user2']); + $this->assertArrayHasKey('email', $customers['user2']); + $this->assertArrayHasKey('address', $customers['user2']); + $this->assertArrayHasKey('status', $customers['user2']); + $this->assertArrayHasKey('id', $customers['user3']); + $this->assertArrayHasKey('name', $customers['user3']); + $this->assertArrayHasKey('email', $customers['user3']); + $this->assertArrayHasKey('address', $customers['user3']); + $this->assertArrayHasKey('status', $customers['user3']); + + // indexBy callable + asArray + $customers = $customerClass::find()->indexBy(function ($customer) { + return $customer['id'] . '-' . $customer['name']; + })->asArray()->all(); + $this->assertCount(3, $customers); + $this->assertArrayHasKey('id', $customers['1-user1']); + $this->assertArrayHasKey('name', $customers['1-user1']); + $this->assertArrayHasKey('email', $customers['1-user1']); + $this->assertArrayHasKey('address', $customers['1-user1']); + $this->assertArrayHasKey('status', $customers['1-user1']); + $this->assertArrayHasKey('id', $customers['2-user2']); + $this->assertArrayHasKey('name', $customers['2-user2']); + $this->assertArrayHasKey('email', $customers['2-user2']); + $this->assertArrayHasKey('address', $customers['2-user2']); + $this->assertArrayHasKey('status', $customers['2-user2']); + $this->assertArrayHasKey('id', $customers['3-user3']); + $this->assertArrayHasKey('name', $customers['3-user3']); + $this->assertArrayHasKey('email', $customers['3-user3']); + $this->assertArrayHasKey('address', $customers['3-user3']); + $this->assertArrayHasKey('status', $customers['3-user3']); + } + + public function testRefresh() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = new $customerClass(); + $this->assertFalse($customer->refresh()); + + $customer = $customerClass::findOne(1); + $customer->name = 'to be refreshed'; + $this->assertTrue($customer->refresh()); + $this->assertEquals('user1', $customer->name); + } + + public function testEquals() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $itemClass ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $customerA = new $customerClass(); + $customerB = new $customerClass(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = new $customerClass(); + $customerB = new $itemClass(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = $customerClass::findOne(1); + $customerB = $customerClass::findOne(2); + $this->assertFalse($customerA->equals($customerB)); + + $customerB = $customerClass::findOne(1); + $this->assertTrue($customerA->equals($customerB)); + + $customerA = $customerClass::findOne(1); + $customerB = $itemClass::findOne(1); + $this->assertFalse($customerA->equals($customerB)); + } + + public function testFindCount() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $this->assertEquals(3, $customerClass::find()->count()); + + $this->assertEquals(1, $customerClass::find()->where(['id' => 1])->count()); + $this->assertEquals(2, $customerClass::find()->where(['id' => [1, 2]])->count()); + $this->assertEquals(2, $customerClass::find()->where(['id' => [1, 2]])->offset(1)->count()); + $this->assertEquals(2, $customerClass::find()->where(['id' => [1, 2]])->offset(2)->count()); + + // limit should have no effect on count() + $this->assertEquals(3, $customerClass::find()->limit(1)->count()); + $this->assertEquals(3, $customerClass::find()->limit(2)->count()); + $this->assertEquals(3, $customerClass::find()->limit(10)->count()); + $this->assertEquals(3, $customerClass::find()->offset(2)->limit(2)->count()); + } + + public function testFindLimit() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + // all() + $customers = $customerClass::find()->all(); + $this->assertCount(3, $customers); + + $customers = $customerClass::find()->orderBy('id')->limit(1)->all(); + $this->assertCount(1, $customers); + $this->assertEquals('user1', $customers[0]->name); + + $customers = $customerClass::find()->orderBy('id')->limit(1)->offset(1)->all(); + $this->assertCount(1, $customers); + $this->assertEquals('user2', $customers[0]->name); + + $customers = $customerClass::find()->orderBy('id')->limit(1)->offset(2)->all(); + $this->assertCount(1, $customers); + $this->assertEquals('user3', $customers[0]->name); + + $customers = $customerClass::find()->orderBy('id')->limit(2)->offset(1)->all(); + $this->assertCount(2, $customers); + $this->assertEquals('user2', $customers[0]->name); + $this->assertEquals('user3', $customers[1]->name); + + $customers = $customerClass::find()->limit(2)->offset(3)->all(); + $this->assertCount(0, $customers); + + // one() + $customer = $customerClass::find()->orderBy('id')->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $customerClass::find()->orderBy('id')->offset(0)->one(); + $this->assertEquals('user1', $customer->name); + + $customer = $customerClass::find()->orderBy('id')->offset(1)->one(); + $this->assertEquals('user2', $customer->name); + + $customer = $customerClass::find()->orderBy('id')->offset(2)->one(); + $this->assertEquals('user3', $customer->name); + + $customer = $customerClass::find()->offset(3)->one(); + $this->assertNull($customer); + } + + public function testFindComplexCondition() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $this->assertEquals(2, $customerClass::find()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->count()); + $this->assertCount(2, $customerClass::find()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->all()); + + $this->assertEquals(2, $customerClass::find()->where(['name' => ['user1', 'user2']])->count()); + $this->assertCount(2, $customerClass::find()->where(['name' => ['user1', 'user2']])->all()); + + $this->assertEquals(1, $customerClass::find()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->count()); + $this->assertCount(1, $customerClass::find()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->all()); + } + + public function testFindNullValues() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = $customerClass::findOne(2); + $customer->name = null; + $customer->save(false); + $this->afterSave(); + + $result = $customerClass::find()->where(['name' => null])->all(); + $this->assertCount(1, $result); + $this->assertEquals(2, reset($result)->primaryKey); + } + + public function testExists() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $this->assertTrue($customerClass::find()->where(['id' => 2])->exists()); + $this->assertFalse($customerClass::find()->where(['id' => 5])->exists()); + $this->assertTrue($customerClass::find()->where(['name' => 'user1'])->exists()); + $this->assertFalse($customerClass::find()->where(['name' => 'user5'])->exists()); + + $this->assertTrue($customerClass::find()->where(['id' => [2, 3]])->exists()); + $this->assertTrue($customerClass::find()->where(['id' => [2, 3]])->offset(1)->exists()); + $this->assertFalse($customerClass::find()->where(['id' => [2, 3]])->offset(2)->exists()); + } + + public function testFindLazy() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = $customerClass::findOne(2); + $this->assertFalse($customer->isRelationPopulated('orders')); + $orders = $customer->orders; + $this->assertTrue($customer->isRelationPopulated('orders')); + $this->assertCount(2, $orders); + $this->assertCount(1, $customer->relatedRecords); + + // unset + unset($customer['orders']); + $this->assertFalse($customer->isRelationPopulated('orders')); + + /* @var $customer Customer */ + $customer = $customerClass::findOne(2); + $this->assertFalse($customer->isRelationPopulated('orders')); + $orders = $customer->getOrders()->where(['id' => 3])->all(); + $this->assertFalse($customer->isRelationPopulated('orders')); + $this->assertCount(0, $customer->relatedRecords); + + $this->assertCount(1, $orders); + $this->assertEquals(3, $orders[0]->id); + } + + public function testFindEager() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $customers = $customerClass::find()->with('orders')->indexBy('id')->all(); + ksort($customers); + $this->assertCount(3, $customers); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); + $this->assertTrue($customers[2]->isRelationPopulated('orders')); + $this->assertTrue($customers[3]->isRelationPopulated('orders')); + $this->assertCount(1, $customers[1]->orders); + $this->assertCount(2, $customers[2]->orders); + $this->assertCount(0, $customers[3]->orders); + // unset + unset($customers[1]->orders); + $this->assertFalse($customers[1]->isRelationPopulated('orders')); + + $customer = $customerClass::find()->where(['id' => 1])->with('orders')->one(); + $this->assertTrue($customer->isRelationPopulated('orders')); + $this->assertCount(1, $customer->orders); + $this->assertCount(1, $customer->relatedRecords); + + // multiple with() calls + $orders = $orderClass::find()->with('customer', 'items')->all(); + $this->assertCount(3, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $orders = $orderClass::find()->with('customer')->with('items')->all(); + $this->assertCount(3, $orders); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + } + + public function testFindLazyVia() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + /* @var $order Order */ + $order = $orderClass::findOne(1); + $this->assertEquals(1, $order->id); + $this->assertCount(2, $order->items); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindLazyVia2() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + /* @var $order Order */ + $order = $orderClass::findOne(1); + $order->id = 100; + $this->assertEquals([], $order->items); + } + + public function testFindEagerViaRelation() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $orders = $orderClass::find()->with('items')->orderBy('id')->all(); + $this->assertCount(3, $orders); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('items')); + $this->assertCount(2, $order->items); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testFindNestedRelation() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + $customers = $customerClass::find()->with('orders', 'orders.items')->indexBy('id')->all(); + ksort($customers); + $this->assertCount(3, $customers); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); + $this->assertTrue($customers[2]->isRelationPopulated('orders')); + $this->assertTrue($customers[3]->isRelationPopulated('orders')); + $this->assertCount(1, $customers[1]->orders); + $this->assertCount(2, $customers[2]->orders); + $this->assertCount(0, $customers[3]->orders); + $this->assertTrue($customers[1]->orders[0]->isRelationPopulated('items')); + $this->assertTrue($customers[2]->orders[0]->isRelationPopulated('items')); + $this->assertTrue($customers[2]->orders[1]->isRelationPopulated('items')); + $this->assertCount(2, $customers[1]->orders[0]->items); + $this->assertCount(3, $customers[2]->orders[0]->items); + $this->assertCount(1, $customers[2]->orders[1]->items); + + $customers = $customerClass::find()->where(['id' => 1])->with('ordersWithItems')->one(); + $this->assertTrue($customers->isRelationPopulated('ordersWithItems')); + $this->assertCount(1, $customers->ordersWithItems); + + /** @var Order $order */ + $order = $customers->ordersWithItems[0]; + $this->assertTrue($order->isRelationPopulated('orderItems')); + $this->assertCount(2, $order->orderItems); + } + + /** + * Ensure ActiveRelationTrait does preserve order of items on find via(). + * + * @see https://github.com/yiisoft/yii2/issues/1310. + */ + public function testFindEagerViaRelationPreserveOrder() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + + /* + Item (name, category_id) + Order (customer_id, created_at, total) + OrderItem (order_id, item_id, quantity, subtotal) + + Result should be the following: + + Order 1: 1, 1325282384, 110.0 + - orderItems: + OrderItem: 1, 1, 1, 30.0 + OrderItem: 1, 2, 2, 40.0 + - itemsInOrder: + Item 1: 'Agile Web Application Development with Yii1.1 and PHP5', 1 + Item 2: 'Yii 1.1 Application Development Cookbook', 1 + + Order 2: 2, 1325334482, 33.0 + - orderItems: + OrderItem: 2, 3, 1, 8.0 + OrderItem: 2, 4, 1, 10.0 + OrderItem: 2, 5, 1, 15.0 + - itemsInOrder: + Item 5: 'Cars', 2 + Item 3: 'Ice Age', 2 + Item 4: 'Toy Story', 2 + Order 3: 2, 1325502201, 40.0 + - orderItems: + OrderItem: 3, 2, 1, 40.0 + - itemsInOrder: + Item 3: 'Ice Age', 2 + */ + $orders = $orderClass::find()->with('itemsInOrder1')->orderBy('created_at')->all(); + $this->assertCount(3, $orders); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertCount(2, $order->itemsInOrder1); + $this->assertEquals(1, $order->itemsInOrder1[0]->id); + $this->assertEquals(2, $order->itemsInOrder1[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertCount(3, $order->itemsInOrder1); + $this->assertEquals(5, $order->itemsInOrder1[0]->id); + $this->assertEquals(3, $order->itemsInOrder1[1]->id); + $this->assertEquals(4, $order->itemsInOrder1[2]->id); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder1')); + $this->assertCount(1, $order->itemsInOrder1); + $this->assertEquals(2, $order->itemsInOrder1[0]->id); + } + + // different order in via table + public function testFindEagerViaRelationPreserveOrderB() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + $orders = $orderClass::find()->with('itemsInOrder2')->orderBy('created_at')->all(); + $this->assertCount(3, $orders); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertCount(2, $order->itemsInOrder2); + $this->assertEquals(1, $order->itemsInOrder2[0]->id); + $this->assertEquals(2, $order->itemsInOrder2[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertCount(3, $order->itemsInOrder2); + $this->assertEquals(5, $order->itemsInOrder2[0]->id); + $this->assertEquals(3, $order->itemsInOrder2[1]->id); + $this->assertEquals(4, $order->itemsInOrder2[2]->id); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertTrue($order->isRelationPopulated('itemsInOrder2')); + $this->assertCount(1, $order->itemsInOrder2); + $this->assertEquals(2, $order->itemsInOrder2[0]->id); + } + + public function testLink() + { + /* @var $orderClass ActiveRecordInterface */ + /* @var $itemClass ActiveRecordInterface */ + /* @var $orderItemClass ActiveRecordInterface */ + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + $orderClass = $this->getOrderClass(); + $orderItemClass = $this->getOrderItemClass(); + $itemClass = $this->getItemClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = $customerClass::findOne(2); + $this->assertCount(2, $customer->orders); + + // has many + $order = new $orderClass(); + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer->link('orders', $order); + $this->afterSave(); + $this->assertCount(3, $customer->orders); + $this->assertFalse($order->isNewRecord); + $this->assertCount(3, $customer->getOrders()->all()); + $this->assertEquals(2, $order->customer_id); + + // belongs to + $order = new $orderClass(); + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer = $customerClass::findOne(1); + $this->assertNull($order->customer); + $order->link('customer', $customer); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(1, $order->customer_id); + $this->assertEquals(1, $order->customer->primaryKey); + + // via model + $order = $orderClass::findOne(1); + $this->assertCount(2, $order->items); + $this->assertCount(2, $order->orderItems); + $orderItem = $orderItemClass::findOne(['order_id' => 1, 'item_id' => 3]); + $this->assertNull($orderItem); + $item = $itemClass::findOne(3); + $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]); + $this->afterSave(); + $this->assertCount(3, $order->items); + $this->assertCount(3, $order->orderItems); + $orderItem = $orderItemClass::findOne(['order_id' => 1, 'item_id' => 3]); + $this->assertInstanceOf($orderItemClass, $orderItem); + $this->assertEquals(10, $orderItem->quantity); + $this->assertEquals(100, $orderItem->subtotal); + } + + public function testUnlink() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + /* @var $orderWithNullFKClass ActiveRecordInterface */ + $orderWithNullFKClass = $this->getOrderWithNullFKClass(); + /* @var $orderItemsWithNullFKClass ActiveRecordInterface */ + $orderItemsWithNullFKClass = $this->getOrderItemWithNullFKmClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + // has many without delete + $customer = $customerClass::findOne(2); + $this->assertCount(2, $customer->ordersWithNullFK); + $customer->unlink('ordersWithNullFK', $customer->ordersWithNullFK[1], false); + + $this->assertCount(1, $customer->ordersWithNullFK); + $orderWithNullFK = $orderWithNullFKClass::findOne(3); + + $this->assertEquals(3, $orderWithNullFK->id); + $this->assertNull($orderWithNullFK->customer_id); + + // has many with delete + $customer = $customerClass::findOne(2); + $this->assertCount(2, $customer->orders); + $customer->unlink('orders', $customer->orders[1], true); + $this->afterSave(); + + $this->assertCount(1, $customer->orders); + $this->assertNull($orderClass::findOne(3)); + + // via model with delete + $order = $orderClass::findOne(2); + $this->assertCount(3, $order->items); + $this->assertCount(3, $order->orderItems); + $order->unlink('items', $order->items[2], true); + $this->afterSave(); + + $this->assertCount(2, $order->items); + $this->assertCount(2, $order->orderItems); + + // via model without delete + $this->assertCount(2, $order->itemsWithNullFK); + $order->unlink('itemsWithNullFK', $order->itemsWithNullFK[1], false); + $this->afterSave(); + + $this->assertCount(1, $order->itemsWithNullFK); + $this->assertCount(2, $order->orderItems); + } + + public function testUnlinkAll() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + /* @var $orderItemClass ActiveRecordInterface */ + $orderItemClass = $this->getOrderItemClass(); + /* @var $itemClass ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + /* @var $orderWithNullFKClass ActiveRecordInterface */ + $orderWithNullFKClass = $this->getOrderWithNullFKClass(); + /* @var $orderItemsWithNullFKClass ActiveRecordInterface */ + $orderItemsWithNullFKClass = $this->getOrderItemWithNullFKmClass(); + + /* @var $this TestCase|ActiveRecordTestTrait */ + // has many with delete + $customer = $customerClass::findOne(2); + $this->assertCount(2, $customer->orders); + $this->assertEquals(3, $orderClass::find()->count()); + $customer->unlinkAll('orders', true); + $this->afterSave(); + $this->assertEquals(1, $orderClass::find()->count()); + $this->assertCount(0, $customer->orders); + + $this->assertNull($orderClass::findOne(2)); + $this->assertNull($orderClass::findOne(3)); + + + // has many without delete + $customer = $customerClass::findOne(2); + $this->assertCount(2, $customer->ordersWithNullFK); + $this->assertEquals(3, $orderWithNullFKClass::find()->count()); + $customer->unlinkAll('ordersWithNullFK', false); + $this->afterSave(); + $this->assertCount(0, $customer->ordersWithNullFK); + $this->assertEquals(3, $orderWithNullFKClass::find()->count()); + $this->assertEquals(2, $orderWithNullFKClass::find()->where(['AND', ['id' => [2, 3]], ['customer_id' => null]])->count()); + + + // via model with delete + /* @var $order Order */ + $order = $orderClass::findOne(1); + $this->assertCount(2, $order->books); + $orderItemCount = $orderItemClass::find()->count(); + $this->assertEquals(5, $itemClass::find()->count()); + $order->unlinkAll('books', true); + $this->afterSave(); + $this->assertEquals(5, $itemClass::find()->count()); + $this->assertEquals($orderItemCount - 2, $orderItemClass::find()->count()); + $this->assertCount(0, $order->books); + + // via model without delete + $this->assertCount(2, $order->booksWithNullFK); + $orderItemCount = $orderItemsWithNullFKClass::find()->count(); + $this->assertEquals(5, $itemClass::find()->count()); + $order->unlinkAll('booksWithNullFK', false); + $this->afterSave(); + $this->assertCount(0, $order->booksWithNullFK); + $this->assertEquals(2, $orderItemsWithNullFKClass::find()->where(['AND', ['item_id' => [1, 2]], ['order_id' => null]])->count()); + $this->assertEquals($orderItemCount, $orderItemsWithNullFKClass::find()->count()); + $this->assertEquals(5, $itemClass::find()->count()); + + // via table is covered in \yiiunit\framework\db\ActiveRecordTest::testUnlinkAllViaTable() + } + + public function testUnlinkAllAndConditionSetNull() + { + /* @var $this TestCase|ActiveRecordTestTrait */ + + /* @var $customerClass \yii\db\BaseActiveRecord */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass \yii\db\BaseActiveRecord */ + $orderClass = $this->getOrderWithNullFKClass(); + + // in this test all orders are owned by customer 1 + $orderClass::updateAll(['customer_id' => 1]); + $this->afterSave(); + + $customer = $customerClass::findOne(1); + $this->assertCount(3, $customer->ordersWithNullFK); + $this->assertCount(1, $customer->expensiveOrdersWithNullFK); + $this->assertEquals(3, $orderClass::find()->count()); + $customer->unlinkAll('expensiveOrdersWithNullFK'); + $this->assertCount(3, $customer->ordersWithNullFK); + $this->assertCount(0, $customer->expensiveOrdersWithNullFK); + $this->assertEquals(3, $orderClass::find()->count()); + $customer = $customerClass::findOne(1); + $this->assertCount(2, $customer->ordersWithNullFK); + $this->assertCount(0, $customer->expensiveOrdersWithNullFK); + } + + public function testUnlinkAllAndConditionDelete() + { + /* @var $this TestCase|ActiveRecordTestTrait */ + + /* @var $customerClass \yii\db\BaseActiveRecord */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass \yii\db\BaseActiveRecord */ + $orderClass = $this->getOrderClass(); + + // in this test all orders are owned by customer 1 + $orderClass::updateAll(['customer_id' => 1]); + $this->afterSave(); + + $customer = $customerClass::findOne(1); + $this->assertCount(3, $customer->orders); + $this->assertCount(1, $customer->expensiveOrders); + $this->assertEquals(3, $orderClass::find()->count()); + $customer->unlinkAll('expensiveOrders', true); + $this->assertCount(3, $customer->orders); + $this->assertCount(0, $customer->expensiveOrders); + $this->assertEquals(2, $orderClass::find()->count()); + $customer = $customerClass::findOne(1); + $this->assertCount(2, $customer->orders); + $this->assertCount(0, $customer->expensiveOrders); + } + + public static $afterSaveNewRecord; + public static $afterSaveInsert; + + public function testInsert() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = new $customerClass(); + $customer->email = 'user4@example.com'; + $customer->name = 'user4'; + $customer->address = 'address4'; + + $this->assertNull($customer->id); + $this->assertTrue($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + + $customer->save(); + $this->afterSave(); + + $this->assertNotNull($customer->id); + $this->assertFalse(static::$afterSaveNewRecord); + $this->assertTrue(static::$afterSaveInsert); + $this->assertFalse($customer->isNewRecord); + } + + public function testExplicitPkOnAutoIncrement() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = new $customerClass(); + $customer->id = 1337; + $customer->email = 'user1337@example.com'; + $customer->name = 'user1337'; + $customer->address = 'address1337'; + + $this->assertTrue($customer->isNewRecord); + $customer->save(); + $this->afterSave(); + + $this->assertEquals(1337, $customer->id); + $this->assertFalse($customer->isNewRecord); + } + + public function testUpdate() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + // save + /* @var $customer Customer */ + $customer = $customerClass::findOne(2); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + $this->assertEmpty($customer->dirtyAttributes); + + $customer->name = 'user2x'; + $customer->save(); + $this->afterSave(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $this->assertFalse(static::$afterSaveNewRecord); + $this->assertFalse(static::$afterSaveInsert); + $customer2 = $customerClass::findOne(2); + $this->assertEquals('user2x', $customer2->name); + + // updateAll + $customer = $customerClass::findOne(3); + $this->assertEquals('user3', $customer->name); + $ret = $customerClass::updateAll(['name' => 'temp'], ['id' => 3]); + $this->afterSave(); + $this->assertEquals(1, $ret); + $customer = $customerClass::findOne(3); + $this->assertEquals('temp', $customer->name); + + $ret = $customerClass::updateAll(['name' => 'tempX']); + $this->afterSave(); + $this->assertEquals(3, $ret); + + $ret = $customerClass::updateAll(['name' => 'temp'], ['name' => 'user6']); + $this->afterSave(); + $this->assertEquals(0, $ret); + } + + public function testUpdateAttributes() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + /* @var $customer Customer */ + $customer = $customerClass::findOne(2); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + static::$afterSaveNewRecord = null; + static::$afterSaveInsert = null; + + $customer->updateAttributes(['name' => 'user2x']); + $this->afterSave(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $this->assertNull(static::$afterSaveNewRecord); + $this->assertNull(static::$afterSaveInsert); + $customer2 = $customerClass::findOne(2); + $this->assertEquals('user2x', $customer2->name); + + $customer = $customerClass::findOne(1); + $this->assertEquals('user1', $customer->name); + $this->assertEquals(1, $customer->status); + $customer->name = 'user1x'; + $customer->status = 2; + $customer->updateAttributes(['name']); + $this->assertEquals('user1x', $customer->name); + $this->assertEquals(2, $customer->status); + $customer = $customerClass::findOne(1); + $this->assertEquals('user1x', $customer->name); + $this->assertEquals(1, $customer->status); + } + + public function testUpdateCounters() + { + /* @var $orderItemClass ActiveRecordInterface */ + $orderItemClass = $this->getOrderItemClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + // updateCounters + $pk = ['order_id' => 2, 'item_id' => 4]; + $orderItem = $orderItemClass::findOne($pk); + $this->assertEquals(1, $orderItem->quantity); + $ret = $orderItem->updateCounters(['quantity' => -1]); + $this->afterSave(); + $this->assertEquals(1, $ret); + $this->assertEquals(0, $orderItem->quantity); + $orderItem = $orderItemClass::findOne($pk); + $this->assertEquals(0, $orderItem->quantity); + + // updateAllCounters + $pk = ['order_id' => 1, 'item_id' => 2]; + $orderItem = $orderItemClass::findOne($pk); + $this->assertEquals(2, $orderItem->quantity); + $ret = $orderItemClass::updateAllCounters([ + 'quantity' => 3, + 'subtotal' => -10, + ], $pk); + $this->afterSave(); + $this->assertEquals(1, $ret); + $orderItem = $orderItemClass::findOne($pk); + $this->assertEquals(5, $orderItem->quantity); + $this->assertEquals(30, $orderItem->subtotal); + } + + public function testDelete() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + // delete + $customer = $customerClass::findOne(2); + $this->assertInstanceOf($customerClass, $customer); + $this->assertEquals('user2', $customer->name); + $customer->delete(); + $this->afterSave(); + $customer = $customerClass::findOne(2); + $this->assertNull($customer); + + // deleteAll + $customers = $customerClass::find()->all(); + $this->assertCount(2, $customers); + $ret = $customerClass::deleteAll(); + $this->afterSave(); + $this->assertEquals(2, $ret); + $customers = $customerClass::find()->all(); + $this->assertCount(0, $customers); + + $ret = $customerClass::deleteAll(); + $this->afterSave(); + $this->assertEquals(0, $ret); + } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + $customer = new $customerClass(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(1, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(0, $customer->status); + + $customers = $customerClass::find()->where(['status' => true])->all(); + $this->assertCount(2, $customers); + + $customers = $customerClass::find()->where(['status' => false])->all(); + $this->assertCount(1, $customers); + } + + public function testAfterFind() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $orderClass BaseActiveRecord */ + $orderClass = $this->getOrderClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + + $afterFindCalls = []; + Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { + /* @var $ar BaseActiveRecord */ + $ar = $event->sender; + $afterFindCalls[] = [\get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; + }); + + $customer = $customerClass::findOne(1); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $customerClass::find()->where(['id' => 1])->one(); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $customerClass::find()->where(['id' => 1])->all(); + $this->assertNotNull($customer); + $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); + $afterFindCalls = []; + + $customer = $customerClass::find()->where(['id' => 1])->with('orders')->all(); + $this->assertNotNull($customer); + $this->assertEquals([ + [$this->getOrderClass(), false, 1, false], + [$customerClass, false, 1, true], + ], $afterFindCalls); + $afterFindCalls = []; + + if ($this instanceof \yiiunit\extensions\redis\ActiveRecordTest) { // TODO redis does not support orderBy() yet + $customer = $customerClass::find()->where(['id' => [1, 2]])->with('orders')->all(); + } else { + // orderBy is needed to avoid random test failure + $customer = $customerClass::find()->where(['id' => [1, 2]])->with('orders')->orderBy('name')->all(); + } + $this->assertNotNull($customer); + $this->assertEquals([ + [$orderClass, false, 1, false], + [$orderClass, false, 2, false], + [$orderClass, false, 3, false], + [$customerClass, false, 1, true], + [$customerClass, false, 2, true], + ], $afterFindCalls); + $afterFindCalls = []; + + Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); + } + + public function testAfterRefresh() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + + $afterRefreshCalls = []; + Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_REFRESH, function ($event) use (&$afterRefreshCalls) { + /* @var $ar BaseActiveRecord */ + $ar = $event->sender; + $afterRefreshCalls[] = [\get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; + }); + + $customer = $customerClass::findOne(1); + $this->assertNotNull($customer); + $customer->refresh(); + $this->assertEquals([[$customerClass, false, 1, false]], $afterRefreshCalls); + $afterRefreshCalls = []; + Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_REFRESH); + } + + public function testFindEmptyInCondition() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + /* @var $this TestCase|ActiveRecordTestTrait */ + + $customers = $customerClass::find()->where(['id' => [1]])->all(); + $this->assertCount(1, $customers); + + $customers = $customerClass::find()->where(['id' => []])->all(); + $this->assertCount(0, $customers); + + $customers = $customerClass::find()->where(['IN', 'id', [1]])->all(); + $this->assertCount(1, $customers); + + $customers = $customerClass::find()->where(['IN', 'id', []])->all(); + $this->assertCount(0, $customers); + } + + public function testFindEagerIndexBy() + { + /* @var $this TestCase|ActiveRecordTestTrait */ + + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var $order Order */ + $order = $orderClass::find()->with('itemsIndexed')->where(['id' => 1])->one(); + $this->assertTrue($order->isRelationPopulated('itemsIndexed')); + $items = $order->itemsIndexed; + $this->assertCount(2, $items); + $this->assertTrue(isset($items[1])); + $this->assertTrue(isset($items[2])); + + /* @var $order Order */ + $order = $orderClass::find()->with('itemsIndexed')->where(['id' => 2])->one(); + $this->assertTrue($order->isRelationPopulated('itemsIndexed')); + $items = $order->itemsIndexed; + $this->assertCount(3, $items); + $this->assertTrue(isset($items[3])); + $this->assertTrue(isset($items[4])); + $this->assertTrue(isset($items[5])); + } + + public function testAttributeAccess() + { + /* @var $customerClass ActiveRecordInterface */ + $customerClass = $this->getCustomerClass(); + $model = new $customerClass(); + + $this->assertTrue($model->canSetProperty('name')); + $this->assertTrue($model->canGetProperty('name')); + $this->assertFalse($model->canSetProperty('unExistingColumn')); + $this->assertFalse(isset($model->name)); + + $model->name = 'foo'; + $this->assertTrue(isset($model->name)); + unset($model->name); + $this->assertNull($model->name); + + // @see https://github.com/yiisoft/yii2-gii/issues/190 + $baseModel = new $customerClass(); + $this->assertFalse($baseModel->hasProperty('unExistingColumn')); + + + /* @var $customer ActiveRecord */ + $customer = new $customerClass(); + $this->assertInstanceOf($customerClass, $customer); + + $this->assertTrue($customer->canGetProperty('id')); + $this->assertTrue($customer->canSetProperty('id')); + + // tests that we really can get and set this property + $this->assertNull($customer->id); + $customer->id = 10; + $this->assertNotNull($customer->id); + + // Let's test relations + $this->assertTrue($customer->canGetProperty('orderItems')); + $this->assertFalse($customer->canSetProperty('orderItems')); + + // Newly created model must have empty relation + $this->assertSame([], $customer->orderItems); + + // does it still work after accessing the relation? + $this->assertTrue($customer->canGetProperty('orderItems')); + $this->assertFalse($customer->canSetProperty('orderItems')); + + try { + /* @var $itemClass ActiveRecordInterface */ + $itemClass = $this->getItemClass(); + $customer->orderItems = [new $itemClass()]; + $this->fail('setter call above MUST throw Exception'); + } catch (\Exception $e) { + // catch exception "Setting read-only property" + $this->assertInstanceOf('yii\base\InvalidCallException', $e); + } + + // related attribute $customer->orderItems didn't change cause it's read-only + $this->assertSame([], $customer->orderItems); + + $this->assertFalse($customer->canGetProperty('non_existing_property')); + $this->assertFalse($customer->canSetProperty('non_existing_property')); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/17089 + */ + public function testViaWithCallable() + { + /* @var $orderClass ActiveRecordInterface */ + $orderClass = $this->getOrderClass(); + + /* @var Order $order */ + $order = $orderClass::findOne(2); + + $expensiveItems = $order->expensiveItemsUsingViaWithCallable; + $cheapItems = $order->cheapItemsUsingViaWithCallable; + + $this->assertCount(2, $expensiveItems); + $this->assertEquals(4, $expensiveItems[0]->id); + $this->assertEquals(5, $expensiveItems[1]->id); + + $this->assertCount(1, $cheapItems); + $this->assertEquals(3, $cheapItems[0]->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; diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index b8aecf771..089c29152 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -1,22 +1,60 @@ -version: '3.8' # NOTE: When using docker-compose for testing, make sure you set 'hostname' to 'redis' in tests/data/config.php services: - PHP: - image: "yiisoftware/yii2-php:7.4-apache" + yii2-redis-php: + image: "yiisoftware/yii2-php:${PHP_VERSION:-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..df839af65 --- /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..fbe2410f2 --- /dev/null +++ b/tests/predis/sentinel/RedisCacheTest.php @@ -0,0 +1,151 @@ +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 testFlushWithSharedDatabase() + { + $instance = $this->getCacheInstance(); + $this->resetCacheInstance(); + $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..221fe925e --- /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->getClient()->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..afe4481c1 --- /dev/null +++ b/tests/predis/sentinel/config/config.php @@ -0,0 +1,26 @@ + [ + 'redis' => [ + '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..de05c98a1 --- /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..c01153c3c --- /dev/null +++ b/tests/predis/sentinel/data/ar/OrderItem.php @@ -0,0 +1,51 @@ +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..bc6551a75 --- /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..522194199 --- /dev/null +++ b/tests/predis/standalone/RedisCacheTest.php @@ -0,0 +1,151 @@ +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 testFlushWithSharedDatabase() + { + $instance = $this->getCacheInstance(); + $this->resetCacheInstance(); + $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..c1358cb6f --- /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->getClient()->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..d0f619566 --- /dev/null +++ b/tests/predis/standalone/config/config.php @@ -0,0 +1,24 @@ + [ + 'redis' => [ + '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..ec5b62075 --- /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..132ba8553 --- /dev/null +++ b/tests/predis/standalone/data/ar/OrderItem.php @@ -0,0 +1,51 @@ +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 @@ +