From e82e32b89d647baaea9078447764167f56593771 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:05:27 -0300
Subject: [PATCH 01/25] Initial commit.
---
.github/workflows/build.yml | 82 +-
composer.json | 20 +-
src/ActiveQuery.php | 579 +++++++++++
src/ActiveRecord.php | 343 +++++++
src/Cache.php | 397 ++++++++
src/Connection.php | 936 ++++++++++++++++++
src/Example.php | 13 -
src/LuaScriptBuilder.php | 446 +++++++++
src/Mutex.php | 165 +++
src/Session.php | 172 ++++
src/SocketException.php | 25 +
tests/ActiveDataProviderTest.php | 37 +
tests/ActiveRecordTest.php | 707 +++++++++++++
tests/ExampleTest.php | 18 -
tests/RedisCacheTest.php | 221 +++++
tests/RedisConnectionTest.php | 347 +++++++
tests/RedisMutexTest.php | 149 +++
tests/RedisSessionTest.php | 86 ++
tests/TestCase.php | 142 +++
tests/UniqueValidatorTest.php | 155 +++
tests/bootstrap.php | 14 +
tests/data/ar/ActiveRecord.php | 25 +
tests/data/ar/Customer.php | 105 ++
tests/data/ar/CustomerQuery.php | 21 +
tests/data/ar/Item.php | 21 +
tests/data/ar/Order.php | 161 +++
tests/data/ar/OrderItem.php | 49 +
tests/data/ar/OrderItemWithNullFK.php | 30 +
tests/data/ar/OrderWithNullFK.php | 30 +
tests/data/config.php | 29 +
tests/docker/docker-compose.yml | 24 +
tests/support/ConnectionWithErrorEmulator.php | 20 +
32 files changed, 5504 insertions(+), 65 deletions(-)
create mode 100644 src/ActiveQuery.php
create mode 100644 src/ActiveRecord.php
create mode 100644 src/Cache.php
create mode 100644 src/Connection.php
delete mode 100644 src/Example.php
create mode 100644 src/LuaScriptBuilder.php
create mode 100644 src/Mutex.php
create mode 100644 src/Session.php
create mode 100644 src/SocketException.php
create mode 100644 tests/ActiveDataProviderTest.php
create mode 100644 tests/ActiveRecordTest.php
delete mode 100644 tests/ExampleTest.php
create mode 100644 tests/RedisCacheTest.php
create mode 100644 tests/RedisConnectionTest.php
create mode 100644 tests/RedisMutexTest.php
create mode 100644 tests/RedisSessionTest.php
create mode 100644 tests/TestCase.php
create mode 100644 tests/UniqueValidatorTest.php
create mode 100644 tests/bootstrap.php
create mode 100644 tests/data/ar/ActiveRecord.php
create mode 100644 tests/data/ar/Customer.php
create mode 100644 tests/data/ar/CustomerQuery.php
create mode 100644 tests/data/ar/Item.php
create mode 100644 tests/data/ar/Order.php
create mode 100644 tests/data/ar/OrderItem.php
create mode 100644 tests/data/ar/OrderItemWithNullFK.php
create mode 100644 tests/data/ar/OrderWithNullFK.php
create mode 100644 tests/data/config.php
create mode 100644 tests/docker/docker-compose.yml
create mode 100644 tests/support/ConnectionWithErrorEmulator.php
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 719d5a9..85505ce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,31 +1,61 @@
on:
- pull_request:
- paths-ignore:
- - 'docs/**'
- - 'README.md'
- - 'CHANGELOG.md'
- - '.gitignore'
- - '.gitattributes'
- - 'infection.json.dist'
- - 'psalm.xml'
-
- push:
- paths-ignore:
- - 'docs/**'
- - 'README.md'
- - 'CHANGELOG.md'
- - '.gitignore'
- - '.gitattributes'
- - 'infection.json.dist'
- - 'psalm.xml'
+ - pull_request
+ - push
name: build
jobs:
- phpunit:
- uses: yiisoft/actions/.github/workflows/phpunit.yml@master
- with:
- os: >-
- ['ubuntu-latest', 'windows-latest']
- php: >-
- ['8.1', '8.2', '8.3']
+ tests:
+ name: PHP ${{ matrix.php }}-redis-${{ matrix.redis }}
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+
+ php:
+ - 8.1
+ - 8.2
+ - 8.3
+
+ redis:
+ - 4
+ - 5
+ - 6
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Start Redis v4
+ 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'
+ tools: composer:v2, pecl
+
+ - name: Install dependencies with Composer
+ run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader
+
+ - name: Run tests with phpunit.
+ if: matrix.php != '8.1'
+ run: vendor/bin/phpunit --colors=always
+
+ - name: Run tests with phpunit and generate coverage.
+ if: matrix.php == '8.1'
+ run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
+
+ - name: Upload coverage to Codecov.
+ if: matrix.php == '8.1'
+ uses: codecov/codecov-action@v3
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.xml
diff --git a/composer.json b/composer.json
index cd5155d..02cdf2f 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,13 @@
{
- "name": "yii2/template",
- "type": "library",
- "description": "_____",
+ "name": "yii2-extensions/redis",
+ "type": "yii2-extension",
+ "description": "Redis Cache, Session and ActiveRecord for the Yii framework",
"keywords": [
- "_____"
+ "yii2",
+ "redis",
+ "active-record",
+ "cache",
+ "session"
],
"license": "mit",
"minimum-stability": "dev",
@@ -15,16 +19,17 @@
"require-dev": {
"maglnet/composer-require-checker": "^4.6",
"phpunit/phpunit": "^10.2",
- "yii2-extensions/phpstan": "dev-main"
+ "yii2-extensions/phpstan": "dev-main",
+ "yiisoft/yii2-dev": "^2.2"
},
"autoload": {
"psr-4": {
- "yii\\template\\": "src"
+ "yii\\redis\\": "src"
}
},
"autoload-dev": {
"psr-4": {
- "yii\\template\\tests\\": "tests"
+ "yiiunit\\extensions\\redis\\": "tests/"
}
},
"extra": {
@@ -40,7 +45,6 @@
},
"scripts": {
"check-dependencies": "composer-require-checker",
- "mutation": "roave-infection-static-analysis-plugin",
"phpstan": "phpstan",
"test": "phpunit"
},
diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php
new file mode 100644
index 0000000..ddb321d
--- /dev/null
+++ b/src/ActiveQuery.php
@@ -0,0 +1,579 @@
+with('orders')->asArray()->all();
+ * ```
+ *
+ * Relational query
+ * ----------------
+ *
+ * In relational context ActiveQuery represents a relation between two Active Record classes.
+ *
+ * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
+ * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
+ * a getter method which calls one of the above methods and returns the created ActiveQuery object.
+ *
+ * A relation is specified by [[link]] which represents the association between columns
+ * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
+ *
+ * If a relation involves a junction table, it may be specified by [[via()]].
+ * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
+ * marks a relation as inverse of another relation.
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class ActiveQuery extends Component implements ActiveQueryInterface
+{
+ use QueryTrait;
+ use ActiveQueryTrait;
+ use ActiveRelationTrait;
+
+ /**
+ * @event Event an event that is triggered when the query is initialized via [[init()]].
+ */
+ const EVENT_INIT = 'init';
+
+
+ /**
+ * Constructor.
+ * @param string $modelClass the model class associated with this query
+ * @param array $config configurations to be applied to the newly created query object
+ */
+ public function __construct($modelClass, $config = [])
+ {
+ $this->modelClass = $modelClass;
+ parent::__construct($config);
+ }
+
+ /**
+ * Initializes the object.
+ * This method is called at the end of the constructor. The default implementation will trigger
+ * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
+ * to ensure triggering of the event.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->trigger(self::EVENT_INIT);
+ }
+
+ /**
+ * Executes the query and returns all results as an array.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
+ */
+ public function all($db = null)
+ {
+ if ($this->emulateExecution) {
+ return [];
+ }
+
+ // TODO add support for orderBy
+ $data = $this->executeScript($db, 'All');
+ if (empty($data)) {
+ return [];
+ }
+ $rows = [];
+ foreach ($data as $dataRow) {
+ $row = [];
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ $row[$dataRow[$i++]] = $dataRow[$i++];
+ }
+
+ $rows[] = $row;
+ }
+ if (empty($rows)) {
+ return [];
+ }
+
+ $models = $this->createModels($rows);
+ if (!empty($this->with)) {
+ $this->findWith($this->with, $models);
+ }
+ if ($this->indexBy !== null) {
+ $indexedModels = [];
+ if (is_string($this->indexBy)) {
+ foreach ($models as $model) {
+ $key = $model[$this->indexBy];
+ $indexedModels[$key] = $model;
+ }
+ } else {
+ foreach ($models as $model) {
+ $key = call_user_func($this->indexBy, $model);
+ $indexedModels[$key] = $model;
+ }
+ }
+ $models = $indexedModels;
+ }
+ if (!$this->asArray) {
+ foreach ($models as $model) {
+ $model->afterFind();
+ }
+ }
+
+ return $models;
+ }
+
+ /**
+ * Executes the query and returns a single row of result.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
+ * the query result may be either an array or an ActiveRecord object. Null will be returned
+ * if the query results in nothing.
+ */
+ public function one($db = null)
+ {
+ if ($this->emulateExecution) {
+ return null;
+ }
+
+ // TODO add support for orderBy
+ $data = $this->executeScript($db, 'One');
+ if (empty($data)) {
+ return null;
+ }
+ $row = [];
+ $c = count($data);
+ for ($i = 0; $i < $c;) {
+ $row[$data[$i++]] = $data[$i++];
+ }
+ if ($this->asArray) {
+ $model = $row;
+ } else {
+ /* @var $class ActiveRecord */
+ $class = $this->modelClass;
+ $model = $class::instantiate($row);
+ $class = get_class($model);
+ $class::populateRecord($model, $row);
+ }
+ if (!empty($this->with)) {
+ $models = [$model];
+ $this->findWith($this->with, $models);
+ $model = $models[0];
+ }
+ if (!$this->asArray) {
+ $model->afterFind();
+ }
+
+ return $model;
+ }
+
+ /**
+ * Returns the number of records.
+ * @param string $q the COUNT expression. This parameter is ignored by this implementation.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return int number of records
+ */
+ public function count($q = '*', $db = null)
+ {
+ if ($this->emulateExecution) {
+ return 0;
+ }
+
+ if ($this->where === null) {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $this->modelClass;
+ if ($db === null) {
+ $db = $modelClass::getDb();
+ }
+
+ return $db->executeCommand('LLEN', [$modelClass::keyPrefix()]);
+ }
+
+ return $this->executeScript($db, 'Count');
+ }
+
+ /**
+ * Returns a value indicating whether the query result contains any row of data.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return bool whether the query result contains any row of data.
+ */
+ public function exists($db = null)
+ {
+ if ($this->emulateExecution) {
+ return false;
+ }
+ return $this->one($db) !== null;
+ }
+
+ /**
+ * Executes the query and returns the first column of the result.
+ * @param string $column name of the column to select
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return array the first column of the query result. An empty array is returned if the query results in nothing.
+ */
+ public function column($column, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return [];
+ }
+
+ // TODO add support for orderBy
+ return $this->executeScript($db, 'Column', $column);
+ }
+
+ /**
+ * Returns the number of records.
+ * @param string $column the column to sum up
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return int number of records
+ */
+ public function sum($column, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return 0;
+ }
+
+ return $this->executeScript($db, 'Sum', $column);
+ }
+
+ /**
+ * Returns the average of the specified column values.
+ * @param string $column the column name or expression.
+ * Make sure you properly quote column names in the expression.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return int the average of the specified column values.
+ */
+ public function average($column, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return 0;
+ }
+ return $this->executeScript($db, 'Average', $column);
+ }
+
+ /**
+ * Returns the minimum of the specified column values.
+ * @param string $column the column name or expression.
+ * Make sure you properly quote column names in the expression.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return int the minimum of the specified column values.
+ */
+ public function min($column, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return null;
+ }
+ return $this->executeScript($db, 'Min', $column);
+ }
+
+ /**
+ * Returns the maximum of the specified column values.
+ * @param string $column the column name or expression.
+ * Make sure you properly quote column names in the expression.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return int the maximum of the specified column values.
+ */
+ public function max($column, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return null;
+ }
+ return $this->executeScript($db, 'Max', $column);
+ }
+
+ /**
+ * Returns the query result as a scalar value.
+ * The value returned will be the specified attribute in the first record of the query results.
+ * @param string $attribute name of the attribute to select
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @return string the value of the specified attribute in the first record of the query result.
+ * Null is returned if the query result is empty.
+ */
+ public function scalar($attribute, $db = null)
+ {
+ if ($this->emulateExecution) {
+ return null;
+ }
+
+ $record = $this->one($db);
+ if ($record !== null) {
+ return $record->hasAttribute($attribute) ? $record->$attribute : null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Executes a script created by [[LuaScriptBuilder]]
+ * @param Connection|null $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @param string $type the type of the script to generate
+ * @param string $columnName
+ * @throws NotSupportedException
+ * @return array|bool|null|string
+ */
+ protected function executeScript($db, $type, $columnName = null)
+ {
+ if ($this->primaryModel !== null) {
+ // lazy loading
+ if ($this->via instanceof self) {
+ // via junction table
+ $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
+ $this->filterByModels($viaModels);
+ } elseif (is_array($this->via)) {
+ // via relation
+ /* @var $viaQuery ActiveQuery */
+ list($viaName, $viaQuery) = $this->via;
+ if ($viaQuery->multiple) {
+ $viaModels = $viaQuery->all();
+ $this->primaryModel->populateRelation($viaName, $viaModels);
+ } else {
+ $model = $viaQuery->one();
+ $this->primaryModel->populateRelation($viaName, $model);
+ $viaModels = $model === null ? [] : [$model];
+ }
+ $this->filterByModels($viaModels);
+ } else {
+ $this->filterByModels([$this->primaryModel]);
+ }
+ }
+
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $this->modelClass;
+
+ if ($db === null) {
+ $db = $modelClass::getDb();
+ }
+
+ // find by primary key if possible. This is much faster than scanning all records
+ if (
+ is_array($this->where)
+ && (
+ (!isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where)))
+ || (isset($this->where[0]) && $this->where[0] === 'in' && $modelClass::isPrimaryKey((array) $this->where[1]))
+ )
+ ) {
+ return $this->findByPk($db, $type, $columnName);
+ }
+
+ $method = 'build' . $type;
+ $script = $db->getLuaScriptBuilder()->$method($this, $columnName);
+
+ return $db->executeCommand('EVAL', [$script, 0]);
+ }
+
+ /**
+ * Fetch by pk if possible as this is much faster
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `db` application component will be used.
+ * @param string $type the type of the script to generate
+ * @param string $columnName
+ * @return array|bool|null|string
+ * @throws \yii\base\InvalidParamException
+ * @throws \yii\base\NotSupportedException
+ */
+ private function findByPk($db, $type, $columnName = null)
+ {
+ $needSort = !empty($this->orderBy) && in_array($type, ['All', 'One', 'Column']);
+ if ($needSort) {
+ if (!is_array($this->orderBy) || count($this->orderBy) > 1) {
+ throw new NotSupportedException(
+ 'orderBy by multiple columns is not currently supported by redis ActiveRecord.'
+ );
+ }
+
+ $k = key($this->orderBy);
+ $v = $this->orderBy[$k];
+ if (is_numeric($k)) {
+ $orderColumn = $v;
+ $orderType = SORT_ASC;
+ } else {
+ $orderColumn = $k;
+ $orderType = $v;
+ }
+ }
+
+ if (isset($this->where[0]) && $this->where[0] === 'in') {
+ $pks = (array) $this->where[2];
+ } elseif (count($this->where) == 1) {
+ $pks = (array) reset($this->where);
+ } else {
+ foreach ($this->where as $values) {
+ if (is_array($values)) {
+ // TODO support composite IN for composite PK
+ throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.');
+ }
+ }
+ $pks = [$this->where];
+ }
+
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $this->modelClass;
+
+ if ($type === 'Count') {
+ $start = 0;
+ $limit = null;
+ } else {
+ $start = ($this->offset === null || $this->offset < 0) ? 0 : $this->offset;
+ $limit = ($this->limit < 0) ? null : $this->limit;
+ }
+ $i = 0;
+ $data = [];
+ $orderArray = [];
+ foreach ($pks as $pk) {
+ if (++$i > $start && ($limit === null || $i <= $start + $limit)) {
+ $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk);
+ $result = $db->executeCommand('HGETALL', [$key]);
+ if (!empty($result)) {
+ $data[] = $result;
+ if ($needSort) {
+ $orderArray[] = $db->executeCommand('HGET', [$key, $orderColumn]);
+ }
+ if ($type === 'One' && $this->orderBy === null) {
+ break;
+ }
+ }
+ }
+ }
+
+ if ($needSort) {
+ $resultData = [];
+ if ($orderType === SORT_ASC) {
+ asort($orderArray, SORT_NATURAL);
+ } else {
+ arsort($orderArray, SORT_NATURAL);
+ }
+ foreach ($orderArray as $orderKey => $orderItem) {
+ $resultData[] = $data[$orderKey];
+ }
+ $data = $resultData;
+ }
+
+ switch ($type) {
+ case 'All':
+ return $data;
+ case 'One':
+ return reset($data);
+ case 'Count':
+ return count($data);
+ case 'Column':
+ $column = [];
+ foreach ($data as $dataRow) {
+ $row = [];
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ $row[$dataRow[$i++]] = $dataRow[$i++];
+ }
+ $column[] = $row[$columnName];
+ }
+
+ return $column;
+ case 'Sum':
+ $sum = 0;
+ foreach ($data as $dataRow) {
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ if ($dataRow[$i++] == $columnName) {
+ $sum += $dataRow[$i];
+ break;
+ }
+ }
+ }
+
+ return $sum;
+ case 'Average':
+ $sum = 0;
+ $count = 0;
+ foreach ($data as $dataRow) {
+ $count++;
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ if ($dataRow[$i++] == $columnName) {
+ $sum += $dataRow[$i];
+ break;
+ }
+ }
+ }
+
+ return $sum / $count;
+ case 'Min':
+ $min = null;
+ foreach ($data as $dataRow) {
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ if ($dataRow[$i++] == $columnName && ($min == null || $dataRow[$i] < $min)) {
+ $min = $dataRow[$i];
+ break;
+ }
+ }
+ }
+
+ return $min;
+ case 'Max':
+ $max = null;
+ foreach ($data as $dataRow) {
+ $c = count($dataRow);
+ for ($i = 0; $i < $c;) {
+ if ($dataRow[$i++] == $columnName && ($max == null || $dataRow[$i] > $max)) {
+ $max = $dataRow[$i];
+ break;
+ }
+ }
+ }
+
+ return $max;
+ }
+ throw new InvalidParamException('Unknown fetch type: ' . $type);
+ }
+}
diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php
new file mode 100644
index 0000000..9258ddd
--- /dev/null
+++ b/src/ActiveRecord.php
@@ -0,0 +1,343 @@
+
+ * @since 2.0
+ */
+class ActiveRecord extends BaseActiveRecord
+{
+ /**
+ * Returns the database connection used by this AR class.
+ * By default, the "redis" application component is used as the database connection.
+ * You may override this method if you want to use a different database connection.
+ * @return Connection the database connection used by this AR class.
+ */
+ public static function getDb()
+ {
+ return Yii::$app->get('redis');
+ }
+
+ /**
+ * @inheritdoc
+ * @return ActiveQuery the newly created [[ActiveQuery]] instance.
+ */
+ public static function find()
+ {
+ return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
+ }
+
+ /**
+ * Returns the primary key name(s) for this AR class.
+ * This method should be overridden by child classes to define the primary key.
+ *
+ * Note that an array should be returned even when it is a single primary key.
+ *
+ * @return string[] the primary keys of this record.
+ */
+ public static function primaryKey()
+ {
+ return ['id'];
+ }
+
+ /**
+ * Returns the list of all attribute names of the model.
+ * This method must be overridden by child classes to define available attributes.
+ * @return array list of attribute names.
+ */
+ public function attributes()
+ {
+ throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.');
+ }
+
+ /**
+ * Declares prefix of the key that represents the keys that store this records in redis.
+ * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]].
+ * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes
+ * 'order_item'. You may override this method if you want different key naming.
+ * @return string the prefix to apply to all AR keys
+ */
+ public static function keyPrefix()
+ {
+ return Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function insert($runValidation = true, $attributes = null)
+ {
+ if ($runValidation && !$this->validate($attributes)) {
+ return false;
+ }
+ if (!$this->beforeSave(true)) {
+ return false;
+ }
+ $db = static::getDb();
+ $values = $this->getDirtyAttributes($attributes);
+ $pk = [];
+ foreach (static::primaryKey() as $key) {
+ $pk[$key] = $values[$key] = $this->getAttribute($key);
+ if ($pk[$key] === null) {
+ // use auto increment if pk is null
+ $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::keyPrefix() . ':s:' . $key]);
+ $this->setAttribute($key, $values[$key]);
+ } elseif (is_numeric($pk[$key])) {
+ // if pk is numeric update auto increment value
+ $currentPk = $db->executeCommand('GET', [static::keyPrefix() . ':s:' . $key]);
+ if ($pk[$key] > $currentPk) {
+ $db->executeCommand('SET', [static::keyPrefix() . ':s:' . $key, $pk[$key]]);
+ }
+ }
+ }
+ // save pk in a findall pool
+ $pk = static::buildKey($pk);
+ $db->executeCommand('RPUSH', [static::keyPrefix(), $pk]);
+
+ $key = static::keyPrefix() . ':a:' . $pk;
+ // save attributes
+ $setArgs = [$key];
+ foreach ($values as $attribute => $value) {
+ // only insert attributes that are not null
+ if ($value !== null) {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ $setArgs[] = $attribute;
+ $setArgs[] = $value;
+ }
+ }
+
+ if (count($setArgs) > 1) {
+ $db->executeCommand('HMSET', $setArgs);
+ }
+
+ $changedAttributes = array_fill_keys(array_keys($values), null);
+ $this->setOldAttributes($values);
+ $this->afterSave(true, $changedAttributes);
+
+ return true;
+ }
+
+ /**
+ * Updates the whole table using the provided attribute values and conditions.
+ * For example, to change the status to be 1 for all customers whose status is 2:
+ *
+ * ~~~
+ * Customer::updateAll(['status' => 1], ['id' => 2]);
+ * ~~~
+ *
+ * @param array $attributes attribute values (name-value pairs) to be saved into the table
+ * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
+ * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ * @return int the number of rows updated
+ */
+ public static function updateAll($attributes, $condition = null)
+ {
+ if (empty($attributes)) {
+ return 0;
+ }
+ $db = static::getDb();
+ $n = 0;
+ foreach (self::fetchPks($condition) as $pk) {
+ $newPk = $pk;
+ $pk = static::buildKey($pk);
+ $key = static::keyPrefix() . ':a:' . $pk;
+ // save attributes
+ $delArgs = [$key];
+ $setArgs = [$key];
+ foreach ($attributes as $attribute => $value) {
+ if (isset($newPk[$attribute])) {
+ $newPk[$attribute] = $value;
+ }
+ if ($value !== null) {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ $setArgs[] = $attribute;
+ $setArgs[] = $value;
+ } else {
+ $delArgs[] = $attribute;
+ }
+ }
+ $newPk = static::buildKey($newPk);
+ $newKey = static::keyPrefix() . ':a:' . $newPk;
+ // rename index if pk changed
+ if ($newPk != $pk) {
+ $db->executeCommand('MULTI');
+ if (count($setArgs) > 1) {
+ $db->executeCommand('HMSET', $setArgs);
+ }
+ if (count($delArgs) > 1) {
+ $db->executeCommand('HDEL', $delArgs);
+ }
+ $db->executeCommand('LINSERT', [static::keyPrefix(), 'AFTER', $pk, $newPk]);
+ $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]);
+ $db->executeCommand('RENAME', [$key, $newKey]);
+ $db->executeCommand('EXEC');
+ } else {
+ if (count($setArgs) > 1) {
+ $db->executeCommand('HMSET', $setArgs);
+ }
+ if (count($delArgs) > 1) {
+ $db->executeCommand('HDEL', $delArgs);
+ }
+ }
+ $n++;
+ }
+
+ return $n;
+ }
+
+ /**
+ * Updates the whole table using the provided counter changes and conditions.
+ * For example, to increment all customers' age by 1,
+ *
+ * ~~~
+ * Customer::updateAllCounters(['age' => 1]);
+ * ~~~
+ *
+ * @param array $counters the counters to be updated (attribute name => increment value).
+ * Use negative values if you want to decrement the counters.
+ * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
+ * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ * @return int the number of rows updated
+ */
+ public static function updateAllCounters($counters, $condition = null)
+ {
+ if (empty($counters)) {
+ return 0;
+ }
+ $db = static::getDb();
+ $n = 0;
+ foreach (self::fetchPks($condition) as $pk) {
+ $key = static::keyPrefix() . ':a:' . static::buildKey($pk);
+ foreach ($counters as $attribute => $value) {
+ $db->executeCommand('HINCRBY', [$key, $attribute, $value]);
+ }
+ $n++;
+ }
+
+ return $n;
+ }
+
+ /**
+ * Deletes rows in the table using the provided conditions.
+ * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
+ *
+ * For example, to delete all customers whose status is 3:
+ *
+ * ~~~
+ * Customer::deleteAll(['status' => 3]);
+ * ~~~
+ *
+ * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
+ * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ * @return int the number of rows deleted
+ */
+ public static function deleteAll($condition = null)
+ {
+ $pks = self::fetchPks($condition);
+ if (empty($pks)) {
+ return 0;
+ }
+
+ $db = static::getDb();
+ $attributeKeys = [];
+ $db->executeCommand('MULTI');
+ foreach ($pks as $pk) {
+ $pk = static::buildKey($pk);
+ $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]);
+ $attributeKeys[] = static::keyPrefix() . ':a:' . $pk;
+ }
+ $db->executeCommand('DEL', $attributeKeys);
+ $result = $db->executeCommand('EXEC');
+
+ return end($result);
+ }
+
+ private static function fetchPks($condition)
+ {
+ $query = static::find();
+ $query->where($condition);
+ $records = $query->asArray()->all(); // TODO limit fetched columns to pk
+ $primaryKey = static::primaryKey();
+
+ $pks = [];
+ foreach ($records as $record) {
+ $pk = [];
+ foreach ($primaryKey as $key) {
+ $pk[$key] = $record[$key];
+ }
+ $pks[] = $pk;
+ }
+
+ return $pks;
+ }
+
+ /**
+ * Builds a normalized key from a given primary key value.
+ *
+ * @param mixed $key the key to be normalized
+ * @return string the generated key
+ */
+ public static function buildKey($key)
+ {
+ if (is_numeric($key)) {
+ return $key;
+ }
+
+ if (is_string($key)) {
+ return ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key);
+ }
+
+ if (is_array($key)) {
+ if (count($key) == 1) {
+ return self::buildKey(reset($key));
+ }
+ ksort($key); // ensure order is always the same
+ $isNumeric = true;
+ foreach ($key as $value) {
+ if (!is_numeric($value)) {
+ $isNumeric = false;
+ }
+ }
+ if ($isNumeric) {
+ return implode('-', $key);
+ }
+ }
+
+ return md5(json_encode($key, JSON_NUMERIC_CHECK));
+ }
+}
diff --git a/src/Cache.php b/src/Cache.php
new file mode 100644
index 0000000..d9a4ad7
--- /dev/null
+++ b/src/Cache.php
@@ -0,0 +1,397 @@
+ Note: It is recommended to use separate [[Connection::$database|database]] for cache and do not share it with
+ * > other components. If you need to share database, you should set [[$shareDatabase]] to `true` and make sure that
+ * > [[$keyPrefix]] has unique value which will allow to distinguish between cache keys and other data in database.
+ *
+ * See [[yii\caching\Cache]] manual for common cache operations that redis Cache supports.
+ *
+ * Unlike the [[yii\caching\Cache]], redis Cache allows the expire parameter of [[set]], [[add]], [[mset]] and [[madd]] to
+ * be a floating point number, so you may specify the time in milliseconds (e.g. 0.1 will be 100 milliseconds).
+ *
+ * To use redis Cache as the cache application component, configure the application as follows,
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'cache' => [
+ * 'class' => 'yii\redis\Cache',
+ * 'redis' => [
+ * 'hostname' => 'localhost',
+ * 'port' => 6379,
+ * 'database' => 0,
+ * ]
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient:
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'cache' => [
+ * 'class' => 'yii\redis\Cache',
+ * // 'redis' => 'redis' // id of the connection application component
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * If you have multiple redis replicas (e.g. AWS ElasticCache Redis) you can configure the cache to
+ * send read operations to the replicas. If no replicas are configured, all operations will be performed on the
+ * master connection configured via the [[redis]] property.
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'cache' => [
+ * 'class' => 'yii\redis\Cache',
+ * 'enableReplicas' => true,
+ * 'replicas' => [
+ * // config for replica redis connections, (default class will be yii\redis\Connection if not provided)
+ * // you can optionally put in master as hostname as well, as all GET operation will use replicas
+ * 'redis',//id of Redis [[Connection]] Component
+ * ['hostname' => 'redis-slave-002.xyz.0001.apse1.cache.amazonaws.com'],
+ * ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
+ * ],
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * If you're using redis in cluster mode and want to use `MGET` and `MSET` effectively, you will need to supply a
+ * [hash tag](https://redis.io/topics/cluster-spec#keys-hash-tags) to allocate cache keys to the same hash slot.
+ *
+ * ~~~
+ * \Yii::$app->cache->multiSet([
+ * 'posts{user1}' => 123,
+ * 'settings{user1}' => [
+ * 'showNickname' => false,
+ * 'sortBy' => 'created_at',
+ * ],
+ * 'unreadMessages{user1}' => 5,
+ * ]);
+ * ~~~
+ *
+ * @property-read bool $isCluster Whether redis is running in cluster mode or not.
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class Cache extends \yii\caching\Cache
+{
+ /**
+ * @var Connection|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
+ * with a Redis [[Connection]] object.
+ */
+ public $redis = 'redis';
+ /**
+ * @var bool whether to enable read / get from redis replicas.
+ * @since 2.0.8
+ * @see $replicas
+ */
+ public $enableReplicas = false;
+ /**
+ * @var array the Redis [[Connection]] configurations for redis replicas.
+ * Each entry is a class configuration, which will be used to instantiate a replica connection.
+ * The default class is [[Connection|yii\redis\Connection]]. You should at least provide a hostname.
+ *
+ * Configuration example:
+ *
+ * ```php
+ * 'replicas' => [
+ * 'redis',
+ * ['hostname' => 'redis-slave-002.xyz.0001.apse1.cache.amazonaws.com'],
+ * ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
+ * ],
+ * ```
+ *
+ * @since 2.0.8
+ * @see $enableReplicas
+ */
+ public $replicas = [];
+ /**
+ * @var bool|null force cluster mode, don't check on every request. If this is null, cluster mode will be checked
+ * once per request whenever the cache is accessed. To disable the check, set to true if cluster mode
+ * should be enabled, or false if it should be disabled.
+ * @since 2.0.11
+ */
+ public $forceClusterMode;
+ /**
+ * @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)
+ * command, component will iterate through all keys in database and remove only these with matching [[$keyPrefix]].
+ * Note that this will no longer be an atomic operation and it is much less efficient than `FLUSHDB` command. It is
+ * recommended to use separate database for cache and leave this value as `false`.
+ * @since 2.0.12
+ */
+ public $shareDatabase = false;
+
+ /**
+ * @var Connection currently active connection.
+ */
+ private $_replica;
+ /**
+ * @var bool remember if redis is in cluster mode for the whole request
+ */
+ private $_isCluster;
+ /**
+ * @var bool if hash tags were supplied for a MGET/MSET operation
+ */
+ private $_hashTagAvailable = false;
+
+
+ /**
+ * Initializes the redis Cache component.
+ * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ * @throws \yii\base\InvalidConfigException if [[redis]] is invalid.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->redis = Instance::ensure($this->redis, Connection::className());
+ }
+
+ /**
+ * Checks whether a specified key exists in the cache.
+ * This can be faster than getting the value from the cache if the data is big.
+ * Note that this method does not check whether the dependency associated
+ * with the cached data, if there is any, has changed. So a call to [[get]]
+ * may return false while exists returns true.
+ * @param mixed $key a key identifying the cached value. This can be a simple string or
+ * a complex data structure consisting of factors representing the key.
+ * @return bool true if a value exists in cache, false if the value is not in the cache or expired.
+ */
+ public function exists($key)
+ {
+ return (bool) $this->redis->executeCommand('EXISTS', [$this->buildKey($key)]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getValue($key)
+ {
+ $value = $this->getReplica()->executeCommand('GET', [$key]);
+ if ($value === null) {
+ return false; // Key is not in the cache or expired
+ }
+
+ return $value;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getValues($keys)
+ {
+ if ($this->isCluster && !$this->_hashTagAvailable) {
+ return parent::getValues($keys);
+ }
+
+ $response = $this->getReplica()->executeCommand('MGET', $keys);
+ $result = [];
+ $i = 0;
+ foreach ($keys as $key) {
+ $result[$key] = $response[$i++];
+ }
+
+ $this->_hashTagAvailable = false;
+
+ return $result;
+ }
+
+ public function buildKey($key)
+ {
+ if (
+ is_string($key)
+ && $this->isCluster
+ && preg_match('/^(.*)({.+})(.*)$/', $key, $matches) === 1) {
+
+ $this->_hashTagAvailable = true;
+
+ return parent::buildKey($matches[1] . $matches[3]) . $matches[2];
+ }
+
+ return parent::buildKey($key);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function setValue($key, $value, $expire)
+ {
+ if ($expire == 0) {
+ return (bool) $this->redis->executeCommand('SET', [$key, $value]);
+ }
+
+ $expire = (int) ($expire * 1000);
+
+ return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function setValues($data, $expire)
+ {
+ if ($this->isCluster && !$this->_hashTagAvailable) {
+ return parent::setValues($data, $expire);
+ }
+
+ $args = [];
+ foreach ($data as $key => $value) {
+ $args[] = $key;
+ $args[] = $value;
+ }
+
+ $failedKeys = [];
+ if ($expire == 0) {
+ $this->redis->executeCommand('MSET', $args);
+ } else {
+ $expire = (int) ($expire * 1000);
+ $this->redis->executeCommand('MULTI');
+ $this->redis->executeCommand('MSET', $args);
+ $index = [];
+ foreach ($data as $key => $value) {
+ $this->redis->executeCommand('PEXPIRE', [$key, $expire]);
+ $index[] = $key;
+ }
+ $result = $this->redis->executeCommand('EXEC');
+ array_shift($result);
+ foreach ($result as $i => $r) {
+ if ($r != 1) {
+ $failedKeys[] = $index[$i];
+ }
+ }
+ }
+
+ $this->_hashTagAvailable = false;
+
+ return $failedKeys;
+ }
+
+ /**
+ * Returns `true` if the redis extension is forced to run in cluster mode through config or the redis command
+ * `CLUSTER INFO` executes successfully, `false` otherwise.
+ *
+ * Setting [[forceClusterMode]] to either `true` or `false` is preferred.
+ * @return bool whether redis is running in cluster mode or not
+ * @since 2.0.11
+ */
+ public function getIsCluster()
+ {
+ if ($this->forceClusterMode !== null) {
+ return $this->forceClusterMode;
+ }
+
+ if ($this->_isCluster === null) {
+ $this->_isCluster = false;
+ try {
+ $this->redis->executeCommand('CLUSTER INFO');
+ $this->_isCluster = true;
+ } catch (Exception $exception) {
+ // if redis is running without cluster support, this command results in:
+ // `ERR This instance has cluster support disabled`
+ // and [[Connection::executeCommand]] throws an exception
+ // we want to ignore it
+ }
+ }
+
+ return $this->_isCluster;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function addValue($key, $value, $expire)
+ {
+ if ($expire == 0) {
+ return (bool) $this->redis->executeCommand('SET', [$key, $value, 'NX']);
+ }
+
+ $expire = (int) ($expire * 1000);
+
+ return (bool) $this->redis->executeCommand('SET', [$key, $value, 'PX', $expire, 'NX']);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function deleteValue($key)
+ {
+ return (bool) $this->redis->executeCommand('DEL', [$key]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function flushValues()
+ {
+ if ($this->shareDatabase) {
+ $cursor = 0;
+ do {
+ list($cursor, $keys) = $this->redis->scan($cursor, 'MATCH', $this->keyPrefix . '*');
+ $cursor = (int) $cursor;
+ if (!empty($keys)) {
+ $this->redis->executeCommand('DEL', $keys);
+ }
+ } while ($cursor !== 0);
+
+ return true;
+ }
+
+ return $this->redis->executeCommand('FLUSHDB');
+ }
+
+ /**
+ * 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
+ * @throws \yii\base\InvalidConfigException
+ */
+ protected function getReplica()
+ {
+ if ($this->enableReplicas === false) {
+ return $this->redis;
+ }
+
+ if ($this->_replica !== null) {
+ return $this->_replica;
+ }
+
+ if (empty($this->replicas)) {
+ return $this->_replica = $this->redis;
+ }
+
+ $replicas = $this->replicas;
+ shuffle($replicas);
+ $config = array_shift($replicas);
+ $this->_replica = Instance::ensure($config, Connection::className());
+ return $this->_replica;
+ }
+}
diff --git a/src/Connection.php b/src/Connection.php
new file mode 100644
index 0000000..fd8e63c
--- /dev/null
+++ b/src/Connection.php
@@ -0,0 +1,936 @@
+
+ * @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.
+ * @property-read LuaScriptBuilder $luaScriptBuilder
+ * @property-read resource|false $socket
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class Connection extends Component
+{
+ /**
+ * @event Event an event that is triggered after a DB connection is established
+ */
+ const EVENT_AFTER_OPEN = 'afterOpen';
+
+ /**
+ * @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'.
+ * If [[unixSocket]] is specified, hostname and [[port]] will be ignored.
+ */
+ public $hostname = 'localhost';
+ /**
+ * @var string the connection scheme used for connecting to the redis server. Defaults to 'tcp'.
+ * @since 2.0.18
+ */
+ public $scheme = 'tcp';
+ /**
+ * @var string if the query gets redirected, use this as the temporary new hostname
+ * @since 2.0.11
+ */
+ public $redirectConnectionString;
+ /**
+ * @var integer the port to use for connecting to the redis server. Default port is 6379.
+ * If [[unixSocket]] is specified, [[hostname]] and port will be ignored.
+ */
+ public $port = 6379;
+ /**
+ * @var string the unix socket path (e.g. `/var/run/redis/redis.sock`) to use for connecting to the redis server.
+ * This can be used instead of [[hostname]] and [[port]] to connect to the server using a unix socket.
+ * If a unix socket path is specified, [[hostname]] and [[port]] will be ignored.
+ * @since 2.0.1
+ */
+ public $unixSocket;
+ /**
+ * @var string|null username for establishing DB connection. Defaults to `null` meaning AUTH command will be performed without username.
+ * Username was introduced in Redis 6.
+ * @link https://redis.io/commands/auth
+ * @link https://redis.io/topics/acl
+ * @since 2.0.16
+ */
+ public $username;
+ /**
+ * @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is sent.
+ * See https://redis.io/commands/auth
+ */
+ public $password;
+ /**
+ * @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0.
+ * Since version 2.0.6 you can disable the SELECT command sent after connection by setting this property to `null`.
+ */
+ public $database = 0;
+ /**
+ * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: `ini_get("default_socket_timeout")`.
+ */
+ public $connectionTimeout;
+ /**
+ * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used.
+ */
+ public $dataTimeout;
+ /**
+ * @var boolean Send sockets over SSL protocol. Default state is false.
+ * @since 2.0.12
+ */
+ public $useSSL = false;
+ /**
+ * @var array PHP context options which are used in the Redis connection stream.
+ * @see https://www.php.net/manual/en/context.ssl.php
+ * @since 2.0.15
+ */
+ public $contextOptions = [];
+ /**
+ * @var integer Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](https://www.php.net/manual/en/function.stream-socket-client.php).
+ * Currently the select of connection flags is limited to `STREAM_CLIENT_CONNECT` (default), `STREAM_CLIENT_ASYNC_CONNECT` and `STREAM_CLIENT_PERSISTENT`.
+ *
+ * > Warning: `STREAM_CLIENT_PERSISTENT` will make PHP reuse connections to the same server. If you are using multiple
+ * > connection objects to refer to different redis [[$database|databases]] on the same [[port]], redis commands may
+ * > get executed on the wrong database. `STREAM_CLIENT_PERSISTENT` is only safe to use if you use only one database.
+ * >
+ * > You may still use persistent connections in this case when disambiguating ports as described
+ * > in [a comment on the PHP manual](https://www.php.net/manual/en/function.stream-socket-client.php#105393)
+ * > e.g. on the connection used for session storage, specify the port as:
+ * >
+ * > ```php
+ * > 'port' => '6379/session'
+ * > ```
+ *
+ * @see https://www.php.net/manual/en/function.stream-socket-client.php
+ * @since 2.0.5
+ */
+ public $socketClientFlags = STREAM_CLIENT_CONNECT;
+ /**
+ * @var integer The number of times a command execution should be retried when a connection failure occurs.
+ * This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
+ * Defaults to 0 meaning no retries on failure.
+ * @since 2.0.7
+ */
+ public $retries = 0;
+ /**
+ * @var integer The retry interval in microseconds to wait between retry.
+ * This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
+ * Defaults to 0 meaning no wait.
+ * @since 2.0.10
+ */
+ public $retryInterval = 0;
+ /**
+ * @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
+ ];
+
+ /**
+ * @var array redis redirect socket connection pool
+ */
+ private $_pool = [];
+
+
+ /**
+ * Closes the connection when this component is being serialized.
+ * @return array
+ */
+ public function __sleep()
+ {
+ $this->close();
+ return array_keys(get_object_vars($this));
+ }
+
+ /**
+ * Return the connection string used to open a socket connection. During a redirect (cluster mode) this will be the
+ * target of the redirect.
+ * @return string socket connection string
+ * @since 2.0.11
+ */
+ public function getConnectionString()
+ {
+ if ($this->unixSocket) {
+ return 'unix://' . $this->unixSocket;
+ }
+
+ return $this->scheme . '://' . ($this->redirectConnectionString ?: "$this->hostname:$this->port");
+ }
+
+ /**
+ * Return the connection resource if a connection to the target has been established before, `false` otherwise.
+ * @return resource|false
+ */
+ public function getSocket()
+ {
+ return ArrayHelper::getValue($this->_pool, $this->connectionString, false);
+ }
+
+ /**
+ * Returns a value indicating whether the DB connection is established.
+ * @return bool whether the DB connection is established
+ */
+ public function getIsActive()
+ {
+ return ArrayHelper::getValue($this->_pool, $this->connectionString, false) !== false;
+ }
+
+ /**
+ * Establishes a DB connection.
+ * It does nothing if a DB connection has already been established.
+ * @throws Exception if connection fails
+ */
+ public function open()
+ {
+ if ($this->socket !== false) {
+ return;
+ }
+
+ $connection = $this->connectionString . ', database=' . $this->database;
+ \Yii::trace('Opening redis DB connection: ' . $connection, __METHOD__);
+ $socket = @stream_socket_client(
+ $this->connectionString,
+ $errorNumber,
+ $errorDescription,
+ $this->connectionTimeout ?: ini_get('default_socket_timeout'),
+ $this->socketClientFlags,
+ stream_context_create($this->contextOptions)
+ );
+
+ if ($socket) {
+ $this->_pool[ $this->connectionString ] = $socket;
+
+ if ($this->dataTimeout !== null) {
+ stream_set_timeout($socket, $timeout = (int) $this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000));
+ }
+ if ($this->useSSL) {
+ stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
+ }
+ if ($this->password !== null) {
+ $this->executeCommand('AUTH', array_filter([$this->username, $this->password]));
+ }
+ if ($this->database !== null) {
+ $this->executeCommand('SELECT', [$this->database]);
+ }
+ $this->initConnection();
+ } else {
+ \Yii::error("Failed to open redis DB connection ($connection): $errorNumber - $errorDescription", __CLASS__);
+ $message = YII_DEBUG ? "Failed to open redis DB connection ($connection): $errorNumber - $errorDescription" : 'Failed to open DB connection.';
+ throw new Exception($message, $errorDescription, $errorNumber);
+ }
+ }
+
+ /**
+ * Closes the currently active DB connection.
+ * It does nothing if the connection is already closed.
+ */
+ public function close()
+ {
+ foreach ($this->_pool as $socket) {
+ $connection = $this->connectionString . ', database=' . $this->database;
+ \Yii::trace('Closing DB connection: ' . $connection, __METHOD__);
+ try {
+ $this->executeCommand('QUIT');
+ } catch (SocketException $e) {
+ // ignore errors when quitting a closed connection
+ }
+ fclose($socket);
+ }
+
+ $this->_pool = [];
+ }
+
+ /**
+ * Initializes the DB connection.
+ * This method is invoked right after the DB connection is established.
+ * The default implementation triggers an [[EVENT_AFTER_OPEN]] event.
+ */
+ protected function initConnection()
+ {
+ $this->trigger(self::EVENT_AFTER_OPEN);
+ }
+
+ /**
+ * Returns the name of the DB driver for the current [[dsn]].
+ * @return string name of the DB driver
+ */
+ public function getDriverName()
+ {
+ return 'redis';
+ }
+
+ /**
+ * @return LuaScriptBuilder
+ */
+ public function getLuaScriptBuilder()
+ {
+ return new LuaScriptBuilder();
+ }
+
+ /**
+ * Allows issuing all supported commands via magic methods.
+ *
+ * ```php
+ * $redis->hmset('test_collection', 'key1', 'val1', 'key2', 'val2')
+ * ```
+ *
+ * @param string $name name of the missing method to execute
+ * @param array $params method call arguments
+ * @return mixed
+ */
+ public function __call($name, $params)
+ {
+ $redisCommand = strtoupper(Inflector::camel2words($name, false));
+ if (in_array($redisCommand, $this->redisCommands)) {
+ return $this->executeCommand($redisCommand, $params);
+ }
+
+ return parent::__call($name, $params);
+ }
+
+ /**
+ * Executes a redis command.
+ * For a list of available commands and their parameters see https://redis.io/commands.
+ *
+ * The params array should contain the params separated by white space, e.g. to execute
+ * `SET mykey somevalue NX` call the following:
+ *
+ * ```php
+ * $redis->executeCommand('SET', ['mykey', 'somevalue', 'NX']);
+ * ```
+ *
+ * @param string $name the name of the command
+ * @param array $params list of parameters for the command
+ * @return array|bool|null|string Dependent on the executed command this method
+ * will return different data types:
+ *
+ * - `true` for commands that return "status reply" with the message `'OK'` or `'PONG'`.
+ * - `string` for commands that return "status reply" that does not have the message `OK` (since version 2.0.1).
+ * - `string` for commands that return "integer reply"
+ * as the value is in the range of a signed 64 bit integer.
+ * - `string` or `null` for commands that return "bulk reply".
+ * - `array` for commands that return "Multi-bulk replies".
+ *
+ * See [redis protocol description](https://redis.io/topics/protocol)
+ * for details on the mentioned reply types.
+ * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
+ */
+ public function executeCommand($name, $params = [])
+ {
+ $this->open();
+
+ $params = array_merge(explode(' ', $name), $params);
+ $command = '*' . count($params) . "\r\n";
+ foreach ($params as $arg) {
+ $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
+ }
+
+ \Yii::trace("Executing Redis Command: {$name}", __METHOD__);
+ if ($this->retries > 0) {
+ $tries = $this->retries;
+ while ($tries-- > 0) {
+ try {
+ return $this->sendRawCommand($command, $params);
+ } catch (SocketException $e) {
+ \Yii::error($e, __METHOD__);
+ // backup retries, fail on commands that fail inside here
+ $retries = $this->retries;
+ $this->retries = 0;
+ $this->close();
+ if ($this->retryInterval > 0) {
+ usleep($this->retryInterval);
+ }
+ try {
+ $this->open();
+ } catch (SocketException $exception) {
+ // Fail to run initial commands, skip current try
+ \Yii::error($exception, __METHOD__);
+ $this->close();
+ } catch (Exception $exception) {
+ $this->close();
+ }
+
+ $this->retries = $retries;
+ }
+ }
+ }
+ return $this->sendRawCommand($command, $params);
+ }
+
+ /**
+ * Sends RAW command string to the server.
+ *
+ * @param string $command command string
+ * @param array $params list of parameters for the command
+ *
+ * @return array|bool|null|string Dependent on the executed command this method
+ * will return different data types:
+ *
+ * - `true` for commands that return "status reply" with the message `'OK'` or `'PONG'`.
+ * - `string` for commands that return "status reply" that does not have the message `OK` (since version 2.0.1).
+ * - `string` for commands that return "integer reply"
+ * as the value is in the range of a signed 64 bit integer.
+ * - `string` or `null` for commands that return "bulk reply".
+ * - `array` for commands that return "Multi-bulk replies".
+ *
+ * See [redis protocol description](https://redis.io/topics/protocol)
+ * for details on the mentioned reply types.
+ * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
+ * @throws SocketException on connection error.
+ */
+ protected function sendRawCommand($command, $params)
+ {
+ $written = @fwrite($this->socket, $command);
+ if ($written === false) {
+ throw new SocketException("Failed to write to socket.\nRedis command was: " . $command);
+ }
+ if ($written !== ($len = mb_strlen($command, '8bit'))) {
+ throw new SocketException("Failed to write to socket. $written of $len bytes written.\nRedis command was: " . $command);
+ }
+
+ return $this->parseResponse($params, $command);
+ }
+
+ /**
+ * @param array $params
+ * @param string|null $command
+ * @return mixed
+ * @throws Exception on error
+ * @throws SocketException
+ */
+ private function parseResponse($params, $command = null)
+ {
+ $prettyCommand = implode(' ', $params);
+
+ if (($line = fgets($this->socket)) === false) {
+ throw new SocketException("Failed to read from socket.\nRedis command was: " . $prettyCommand);
+ }
+ $type = $line[0];
+ $line = mb_substr($line, 1, -2, '8bit');
+ switch ($type) {
+ case '+': // Status reply
+ if ($line === 'OK' || $line === 'PONG') {
+ return true;
+ }
+
+ return $line;
+ case '-': // Error reply
+
+ if ($this->isRedirect($line)) {
+ return $this->redirect($line, $command, $params);
+ }
+
+ throw new Exception("Redis error: " . $line . "\nRedis command was: " . $prettyCommand);
+ case ':': // Integer reply
+ // no cast to int as it is in the range of a signed 64 bit integer
+ return $line;
+ case '$': // Bulk replies
+ if ($line == '-1') {
+ return null;
+ }
+ $length = (int)$line + 2;
+ $data = '';
+ while ($length > 0) {
+ if (($block = fread($this->socket, $length)) === false) {
+ throw new SocketException("Failed to read from socket.\nRedis command was: " . $prettyCommand);
+ }
+ $data .= $block;
+ $length -= mb_strlen($block, '8bit');
+ }
+
+ return mb_substr($data, 0, -2, '8bit');
+ case '*': // Multi-bulk replies
+ $count = (int) $line;
+ $data = [];
+ for ($i = 0; $i < $count; $i++) {
+ $data[] = $this->parseResponse($params);
+ }
+
+ return $data;
+ default:
+ throw new Exception('Received illegal data from redis: ' . $line . "\nRedis command was: " . $prettyCommand);
+ }
+ }
+
+ /**
+ * @param string $line
+ * @return bool
+ */
+ private function isRedirect($line)
+ {
+ return is_string($line) && mb_strpos($line, 'MOVED') === 0;
+ }
+
+ /**
+ * @param string $redirect
+ * @param string $command
+ * @param array $params
+ * @return mixed
+ * @throws Exception
+ * @throws SocketException
+ */
+ private function redirect($redirect, $command, $params)
+ {
+ $responseParts = preg_split('/\s+/', $redirect);
+
+ $this->redirectConnectionString = ArrayHelper::getValue($responseParts, 2);
+
+ if ($this->redirectConnectionString) {
+ \Yii::info('Redirecting to ' . $this->connectionString, __METHOD__);
+
+ $this->open();
+
+ $response = $this->sendRawCommand($command, $params);
+
+ $this->redirectConnectionString = null;
+
+ return $response;
+ }
+
+ throw new Exception('No hostname found in redis redirect (MOVED): ' . VarDumper::dumpAsString($redirect));
+ }
+}
diff --git a/src/Example.php b/src/Example.php
deleted file mode 100644
index 067eeb8..0000000
--- a/src/Example.php
+++ /dev/null
@@ -1,13 +0,0 @@
-
+ * @since 2.0
+ */
+class LuaScriptBuilder extends \yii\base\BaseObject
+{
+ /**
+ * Builds a Lua script for finding a list of records
+ * @param ActiveQuery $query the query used to build the script
+ * @return string
+ */
+ public function buildAll($query)
+ {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks');
+ }
+
+ /**
+ * Builds a Lua script for finding one record
+ * @param ActiveQuery $query the query used to build the script
+ * @return string
+ */
+ public function buildOne($query)
+ {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks');
+ }
+
+ /**
+ * Builds a Lua script for finding a column
+ * @param ActiveQuery $query the query used to build the script
+ * @param string $column name of the column
+ * @return string
+ */
+ public function buildColumn($query, $column)
+ {
+ // TODO add support for indexBy
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks');
+ }
+
+ /**
+ * Builds a Lua script for getting count of records
+ * @param ActiveQuery $query the query used to build the script
+ * @return string
+ */
+ public function buildCount($query)
+ {
+ return $this->build($query, 'n=n+1', 'n');
+ }
+
+ /**
+ * Builds a Lua script for finding the sum of a column
+ * @param ActiveQuery $query the query used to build the script
+ * @param string $column name of the column
+ * @return string
+ */
+ public function buildSum($query, $column)
+ {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n');
+ }
+
+ /**
+ * Builds a Lua script for finding the average of a column
+ * @param ActiveQuery $query the query used to build the script
+ * @param string $column name of the column
+ * @return string
+ */
+ public function buildAverage($query, $column)
+ {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n');
+ }
+
+ /**
+ * Builds a Lua script for finding the min value of a column
+ * @param ActiveQuery $query the query used to build the script
+ * @param string $column name of the column
+ * @return string
+ */
+ public function buildMin($query, $column)
+ {
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
+
+ return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v');
+ }
+
+ /**
+ * @param ActiveQuery $query the query used to build the script
+ * @param string $buildResult the lua script for building the result
+ * @param string $return the lua variable that should be returned
+ * @throws NotSupportedException when query contains unsupported order by condition
+ * @return string
+ */
+ private function build($query, $buildResult, $return)
+ {
+ $columns = [];
+ if ($query->where !== null) {
+ $condition = $this->buildCondition($query->where, $columns);
+ } else {
+ $condition = 'true';
+ }
+
+ $start = ($query->offset === null || $query->offset < 0) ? 0 : $query->offset;
+ $limitCondition = 'i>' . $start . (($query->limit === null || $query->limit < 0) ? '' : ' and i<=' . ($start + $query->limit));
+
+ /* @var $modelClass ActiveRecord */
+ $modelClass = $query->modelClass;
+ $key = $this->quoteValue($modelClass::keyPrefix());
+ $loadColumnValues = '';
+ foreach ($columns as $column => $alias) {
+ $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, " . $this->quoteValue($column) . ")\n";
+ }
+
+ $getAllPks = <<orderBy)) {
+ if (!is_array($query->orderBy) || count($query->orderBy) > 1) {
+ throw new NotSupportedException(
+ 'orderBy by multiple columns is not currently supported by redis ActiveRecord.'
+ );
+ }
+
+ $k = key($query->orderBy);
+ $v = $query->orderBy[$k];
+ if (is_numeric($k)) {
+ $orderColumn = $v;
+ $orderType = 'ASC';
+ } else {
+ $orderColumn = $k;
+ $orderType = $v === SORT_DESC ? 'DESC' : 'ASC';
+ }
+
+ $getAllPks = <<' .. '$orderColumn', '$orderType')
+if allpks['err'] then
+ allpks=redis.pcall('SORT', $key, 'BY', $key .. ':a:*->' .. '$orderColumn', '$orderType', 'ALPHA')
+end
+EOF;
+ }
+
+ return << 'buildNotCondition',
+ 'and' => 'buildAndCondition',
+ 'or' => 'buildAndCondition',
+ 'between' => 'buildBetweenCondition',
+ 'not between' => 'buildBetweenCondition',
+ 'in' => 'buildInCondition',
+ 'not in' => 'buildInCondition',
+ 'like' => 'buildLikeCondition',
+ 'not like' => 'buildLikeCondition',
+ 'or like' => 'buildLikeCondition',
+ 'or not like' => 'buildLikeCondition',
+ '>' => 'buildCompareCondition',
+ '>=' => 'buildCompareCondition',
+ '<' => 'buildCompareCondition',
+ '<=' => 'buildCompareCondition',
+ ];
+
+ if (!is_array($condition)) {
+ throw new NotSupportedException('Where condition must be an array in redis ActiveRecord.');
+ }
+ if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
+ $operator = strtolower($condition[0]);
+ if (isset($builders[$operator])) {
+ $method = $builders[$operator];
+ array_shift($condition);
+
+ return $this->$method($operator, $condition, $columns);
+ }
+
+ throw new Exception('Found unknown operator in query: ' . $operator);
+ }
+
+ // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
+ return $this->buildHashCondition($condition, $columns);
+ }
+
+ private function buildHashCondition($condition, &$columns)
+ {
+ $parts = [];
+ foreach ($condition as $column => $value) {
+ if (is_array($value)) { // IN condition
+ $parts[] = $this->buildInCondition('in', [$column, $value], $columns);
+ } else {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ if ($value === null) {
+ $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ } elseif ($value instanceof Expression) {
+ $column = $this->addColumn($column, $columns);
+ $parts[] = "$column==" . $value->expression;
+ } else {
+ $column = $this->addColumn($column, $columns);
+ $value = $this->quoteValue($value);
+ $parts[] = "$column==$value";
+ }
+ }
+ }
+
+ return count($parts) === 1 ? $parts[0] : '(' . implode(') and (', $parts) . ')';
+ }
+
+ private function buildNotCondition($operator, $operands, &$params)
+ {
+ if (count($operands) != 1) {
+ throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
+ }
+
+ $operand = reset($operands);
+ if (is_array($operand)) {
+ $operand = $this->buildCondition($operand, $params);
+ }
+
+ return "$operator ($operand)";
+ }
+
+ private function buildAndCondition($operator, $operands, &$columns)
+ {
+ $parts = [];
+ foreach ($operands as $operand) {
+ if (is_array($operand)) {
+ $operand = $this->buildCondition($operand, $columns);
+ }
+ if ($operand !== '') {
+ $parts[] = $operand;
+ }
+ }
+ if (!empty($parts)) {
+ return '(' . implode(") $operator (", $parts) . ')';
+ }
+
+ return '';
+ }
+
+ private function buildBetweenCondition($operator, $operands, &$columns)
+ {
+ if (!isset($operands[0], $operands[1], $operands[2])) {
+ throw new Exception("Operator '$operator' requires three operands.");
+ }
+
+ list($column, $value1, $value2) = $operands;
+
+ $value1 = $this->quoteValue($value1);
+ $value2 = $this->quoteValue($value2);
+ $column = $this->addColumn($column, $columns);
+
+ $condition = "$column >= $value1 and $column <= $value2";
+ return $operator === 'not between' ? "not ($condition)" : $condition;
+ }
+
+ private function buildInCondition($operator, $operands, &$columns)
+ {
+ if (!isset($operands[0], $operands[1])) {
+ throw new Exception("Operator '$operator' requires two operands.");
+ }
+
+ list($column, $values) = $operands;
+
+ $values = (array) $values;
+
+ if (empty($values) || $column === []) {
+ return $operator === 'in' ? 'false' : 'true';
+ }
+
+ if (is_array($column) && count($column) > 1) {
+ return $this->buildCompositeInCondition($operator, $column, $values, $columns);
+ }
+
+ if (is_array($column)) {
+ $column = reset($column);
+ }
+ $columnAlias = $this->addColumn($column, $columns);
+ $parts = [];
+ foreach ($values as $value) {
+ if (is_array($value)) {
+ $value = isset($value[$column]) ? $value[$column] : null;
+ }
+ if ($value === null) {
+ $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ } elseif ($value instanceof Expression) {
+ $parts[] = "$columnAlias==" . $value->expression;
+ } else {
+ $value = $this->quoteValue($value);
+ $parts[] = "$columnAlias==$value";
+ }
+ }
+ $operator = $operator === 'in' ? '' : 'not ';
+
+ return "$operator(" . implode(' or ', $parts) . ')';
+ }
+
+ protected function buildCompositeInCondition($operator, $inColumns, $values, &$columns)
+ {
+ $vss = [];
+ foreach ($values as $value) {
+ $vs = [];
+ foreach ($inColumns as $column) {
+ if (isset($value[$column])) {
+ $columnAlias = $this->addColumn($column, $columns);
+ $vs[] = "$columnAlias==" . $this->quoteValue($value[$column]);
+ } else {
+ $vs[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ }
+ }
+ $vss[] = '(' . implode(' and ', $vs) . ')';
+ }
+ $operator = $operator === 'in' ? '' : 'not ';
+
+ return "$operator(" . implode(' or ', $vss) . ')';
+ }
+
+ protected function buildCompareCondition($operator, $operands, &$columns)
+ {
+ if (!isset($operands[0], $operands[1])) {
+ throw new Exception("Operator '$operator' requires two operands.");
+ }
+
+ list($column, $value) = $operands;
+
+ $column = $this->addColumn($column, $columns);
+ if (is_numeric($value)){
+ return "tonumber($column) $operator $value";
+ }
+ $value = $this->quoteValue($value);
+ return "$column $operator $value";
+ }
+
+ private function buildLikeCondition($operator, $operands, &$columns)
+ {
+ throw new NotSupportedException('LIKE conditions are not suppoerted by redis ActiveRecord.');
+ }
+}
diff --git a/src/Mutex.php b/src/Mutex.php
new file mode 100644
index 0000000..78a8d6c
--- /dev/null
+++ b/src/Mutex.php
@@ -0,0 +1,165 @@
+ [
+ * 'mutex' => [
+ * 'class' => 'yii\redis\Mutex',
+ * 'redis' => [
+ * 'hostname' => 'localhost',
+ * 'port' => 6379,
+ * 'database' => 0,
+ * ]
+ * ],
+ * ],
+ * ]
+ * ```
+ *
+ * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient:
+ *
+ * ```php
+ * [
+ * 'components' => [
+ * 'mutex' => [
+ * 'class' => 'yii\redis\Mutex',
+ * // 'redis' => 'redis' // id of the connection application component
+ * ],
+ * ],
+ * ]
+ * ```
+ *
+ * @see \yii\mutex\Mutex
+ * @see https://redis.io/topics/distlock
+ *
+ * @author Sergey Makinen
+ * @author Alexander Zhuravlev
+ * @since 2.0.6
+ */
+class Mutex extends \yii\mutex\Mutex
+{
+ use RetryAcquireTrait;
+
+ /**
+ * @var int the number of seconds in which the lock will be auto released.
+ */
+ public $expire = 30;
+ /**
+ * @var string a string prefixed to every cache key so that it is unique. If not set,
+ * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string
+ * if you don't want to use key prefix. It is recommended that you explicitly set this property to some
+ * static value if the cached data needs to be shared among multiple applications.
+ */
+ public $keyPrefix;
+ /**
+ * @var Connection|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
+ * with a Redis [[Connection]] object.
+ */
+ public $redis = 'redis';
+
+ /**
+ * @var array Redis lock values. Used to be safe that only a lock owner can release it.
+ */
+ private $_lockValues = [];
+
+
+ /**
+ * Initializes the redis Mutex component.
+ * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ * @throws InvalidConfigException if [[redis]] is invalid.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->redis = Instance::ensure($this->redis, Connection::className());
+ if ($this->keyPrefix === null) {
+ $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
+ }
+ }
+
+ /**
+ * Acquires a lock by name.
+ * @param string $name of the lock to be acquired. Must be unique.
+ * @param int $timeout time (in seconds) to wait for lock to be released. Defaults to `0` meaning that method will return
+ * false immediately in case lock was already acquired.
+ * @return bool lock acquiring result.
+ */
+ protected function acquireLock($name, $timeout = 0)
+ {
+ $key = $this->calculateKey($name);
+ $value = Yii::$app->security->generateRandomString(20);
+
+ $result = $this->retryAcquire($timeout, function () use ($key, $value) {
+ return $this->redis->executeCommand('SET', [$key, $value, 'NX', 'PX', (int) ($this->expire * 1000)]);
+ });
+
+ if ($result) {
+ $this->_lockValues[$name] = $value;
+ }
+ return $result;
+ }
+
+ /**
+ * Releases acquired lock. This method will return `false` in case the lock was not found or Redis command failed.
+ * @param string $name of the lock to be released. This lock must already exist.
+ * @return bool lock release result: `false` in case named lock was not found or Redis command failed.
+ */
+ protected function releaseLock($name)
+ {
+ static $releaseLuaScript = <<_lockValues[$name])
+ || !$this->redis->executeCommand('EVAL', [
+ $releaseLuaScript,
+ 1,
+ $this->calculateKey($name),
+ $this->_lockValues[$name],
+ ])
+ ) {
+ return false;
+ }
+
+ unset($this->_lockValues[$name]);
+ return true;
+ }
+
+ /**
+ * Generates a unique key used for storing the mutex in Redis.
+ * @param string $name mutex name.
+ * @return string a safe cache key associated with the mutex name.
+ */
+ protected function calculateKey($name)
+ {
+ return $this->keyPrefix . md5(json_encode([__CLASS__, $name]));
+ }
+}
diff --git a/src/Session.php b/src/Session.php
new file mode 100644
index 0000000..893b27d
--- /dev/null
+++ b/src/Session.php
@@ -0,0 +1,172 @@
+ [
+ * 'session' => [
+ * 'class' => 'yii\redis\Session',
+ * 'redis' => [
+ * 'hostname' => 'localhost',
+ * 'port' => 6379,
+ * 'database' => 0,
+ * ]
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient:
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'session' => [
+ * 'class' => 'yii\redis\Session',
+ * // 'redis' => 'redis' // id of the connection application component
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * @property-read bool $useCustomStorage Whether to use custom storage.
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class Session extends \yii\web\Session
+{
+ /**
+ * @var Connection|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
+ * with a Redis [[Connection]] object.
+ */
+ public $redis = 'redis';
+ /**
+ * @var string a string prefixed to every cache key so that it is unique. If not set,
+ * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string
+ * if you don't want to use key prefix. It is recommended that you explicitly set this property to some
+ * static value if the cached data needs to be shared among multiple applications.
+ */
+ public $keyPrefix;
+
+
+ /**
+ * Initializes the redis Session component.
+ * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ * @throws InvalidConfigException if [[redis]] is invalid.
+ */
+ public function init()
+ {
+ $this->redis = Instance::ensure($this->redis, Connection::className());
+ if ($this->keyPrefix === null) {
+ $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
+ }
+ parent::init();
+ }
+
+ /**
+ * Returns a value indicating whether to use custom session storage.
+ * This method overrides the parent implementation and always returns true.
+ * @return bool whether to use custom storage.
+ */
+ public function getUseCustomStorage()
+ {
+ return true;
+ }
+
+ /**
+ * Session open handler.
+ * @internal Do not call this method directly.
+ * @param string $savePath session save path
+ * @param string $sessionName session name
+ * @return bool whether session is opened successfully
+ */
+ public function openSession($savePath, $sessionName)
+ {
+ if ($this->getUseStrictMode()) {
+ $id = $this->getId();
+ if (!$this->redis->exists($this->calculateKey($id))) {
+ //This session id does not exist, mark it for forced regeneration
+ $this->_forceRegenerateId = $id;
+ }
+ }
+
+ return parent::openSession($savePath, $sessionName);
+ }
+
+ /**
+ * Session read handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @return string the session data
+ */
+ public function readSession($id)
+ {
+ $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]);
+
+ return $data === false || $data === null ? '' : $data;
+ }
+
+ /**
+ * Session write handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @param string $data session data
+ * @return bool whether session write is successful
+ */
+ public function writeSession($id, $data)
+ {
+ if ($this->getUseStrictMode() && $id === $this->_forceRegenerateId) {
+ //Ignore write when forceRegenerate is active for this id
+ return true;
+ }
+
+ return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]);
+ }
+
+ /**
+ * Session destroy handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @return bool whether session is destroyed successfully
+ */
+ public function destroySession($id)
+ {
+ $this->redis->executeCommand('DEL', [$this->calculateKey($id)]);
+ // @see https://github.com/yiisoft/yii2-redis/issues/82
+ return true;
+ }
+
+ /**
+ * Generates a unique key used for storing session data in cache.
+ * @param string $id session variable name
+ * @return string a safe cache key associated with the session variable name
+ */
+ protected function calculateKey($id)
+ {
+ return $this->keyPrefix . md5(json_encode([__CLASS__, $id]));
+ }
+}
diff --git a/src/SocketException.php b/src/SocketException.php
new file mode 100644
index 0000000..d286ad7
--- /dev/null
+++ b/src/SocketException.php
@@ -0,0 +1,25 @@
+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/ActiveRecordTest.php b/tests/ActiveRecordTest.php
new file mode 100644
index 0000000..0b7fa46
--- /dev/null
+++ b/tests/ActiveRecordTest.php
@@ -0,0 +1,707 @@
+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()
+ {
+ /* @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()
+ {
+ // 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()
+ {
+ // 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()
+ {
+ // 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()
+ {
+ $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()
+ {
+ 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()
+ {
+ $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()
+ {
+ Order::getDb()->flushdb();
+ $orders = Order::find()
+ ->where(['total' => 100000])
+ ->orWhere(['total' => 1])
+ ->with('customer')
+ ->all();
+ $this->assertEquals([], $orders);
+ }
+
+ public function testEmulateExecution()
+ {
+ $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()
+ {
+ $deletedCount = Order::deleteAll(['in', 'id', [1, 2, 3]]);
+ $this->assertEquals(3, $deletedCount);
+ }
+
+ public function testBuildKey()
+ {
+ $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()
+ {
+ /* @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()
+ {
+ /* @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()
+ {
+ /* @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()
+ {
+ /* @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()
+ {
+ 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 = [])
+ {
+ /* @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->assertContains($string, $script);
+ }
+ foreach($unexpectedStrings as $string) {
+ $this->assertNotContains($string, $script);
+ }
+ }
+
+ public function illegalValuesForFindByCondition()
+ {
+ 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 = [])
+ {
+ /* @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->assertContains($string, $script);
+ }
+ foreach($unexpectedStrings as $string) {
+ $this->assertNotContains($string, $script);
+ }
+ // ensure injected FLUSHALL call did not succeed
+ $query->one();
+ $this->assertGreaterThan(3, $itemClass::find()->count());
+ }
+
+ public function testCompareCondition()
+ {
+ /* @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()
+ {
+ /* @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()
+ {
+ /* @var $customerClass \yii\db\ActiveRecordInterface */
+ $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/ExampleTest.php b/tests/ExampleTest.php
deleted file mode 100644
index 2825a4e..0000000
--- a/tests/ExampleTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-assertTrue($example->getExample());
- }
-}
diff --git a/tests/RedisCacheTest.php b/tests/RedisCacheTest.php
new file mode 100644
index 0000000..548727a
--- /dev/null
+++ b/tests/RedisCacheTest.php
@@ -0,0 +1,221 @@
+markTestSkipped('No redis server connection configured.');
+ }
+ $connection = new Connection($params);
+// if (!@stream_socket_client($connection->hostname . ':' . $connection->port, $errorNumber, $errorDescription, 0.5)) {
+// $this->markTestSkipped('No redis server running at ' . $connection->hostname . ':' . $connection->port . ' : ' . $errorNumber . ' - ' . $errorDescription);
+// }
+
+ $this->mockApplication(['components' => ['redis' => $connection]]);
+
+ if ($this->_cacheInstance === null) {
+ $this->_cacheInstance = new Cache();
+ }
+
+ return $this->_cacheInstance;
+ }
+
+ protected function resetCacheInstance()
+ {
+ $this->getCacheInstance()->redis->flushdb();
+ $this->_cacheInstance = null;
+ }
+
+ public function testExpireMilliseconds()
+ {
+ $cache = $this->getCacheInstance();
+
+ $this->assertTrue($cache->set('expire_test_ms', 'expire_test_ms', 0.2));
+ usleep(100000);
+ $this->assertEquals('expire_test_ms', $cache->get('expire_test_ms'));
+ usleep(300000);
+ $this->assertFalse($cache->get('expire_test_ms'));
+ }
+
+ public function testExpireAddMilliseconds()
+ {
+ $cache = $this->getCacheInstance();
+
+ $this->assertTrue($cache->add('expire_testa_ms', 'expire_testa_ms', 0.2));
+ usleep(100000);
+ $this->assertEquals('expire_testa_ms', $cache->get('expire_testa_ms'));
+ usleep(300000);
+ $this->assertFalse($cache->get('expire_testa_ms'));
+ }
+
+ /**
+ * Store a value that is 2 times buffer size big
+ * https://github.com/yiisoft/yii2/issues/743
+ */
+ public function testLargeData()
+ {
+ $cache = $this->getCacheInstance();
+
+ $data = str_repeat('XX', 8192); // https://www.php.net/manual/en/function.fread.php
+ $key = 'bigdata1';
+
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $data);
+ $this->assertSame($cache->get($key), $data);
+
+ // try with multibyte string
+ $data = str_repeat('ЖЫ', 8192); // https://www.php.net/manual/en/function.fread.php
+ $key = 'bigdata2';
+
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $data);
+ $this->assertSame($cache->get($key), $data);
+ }
+
+ /**
+ * Store a megabyte and see how it goes
+ * https://github.com/yiisoft/yii2/issues/6547
+ */
+ public function testReallyLargeData()
+ {
+ $cache = $this->getCacheInstance();
+
+ $keys = [];
+ for ($i = 1; $i < 16; $i++) {
+ $key = 'realbigdata' . $i;
+ $data = str_repeat('X', 100 * 1024); // 100 KB
+ $keys[$key] = $data;
+
+// $this->assertTrue($cache->get($key) === false); // do not display 100KB in terminal if this fails :)
+ $cache->set($key, $data);
+ }
+ $values = $cache->multiGet(array_keys($keys));
+ foreach ($keys as $key => $value) {
+ $this->assertArrayHasKey($key, $values);
+ $this->assertSame($values[$key], $value);
+ }
+ }
+
+ public function testMultiByteGetAndSet()
+ {
+ $cache = $this->getCacheInstance();
+
+ $data = ['abc' => 'ежик', 2 => 'def'];
+ $key = 'data1';
+
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $data);
+ $this->assertSame($cache->get($key), $data);
+ }
+
+ public function testReplica()
+ {
+ $this->resetCacheInstance();
+
+ $cache = $this->getCacheInstance();
+ $cache->enableReplicas = true;
+
+ $key = 'replica-1';
+ $value = 'replica';
+
+ //No Replica listed
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $value);
+ $this->assertSame($cache->get($key), $value);
+
+ $databases = TestCase::getParam('databases');
+ $redis = isset($databases['redis']) ? $databases['redis'] : null;
+
+ $cache->replicas = [
+ [
+ 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
+ 'password' => isset($redis['password']) ? $redis['password'] : null,
+ ],
+ ];
+ $this->assertSame($cache->get($key), $value);
+
+ //One Replica listed
+ $this->resetCacheInstance();
+ $cache = $this->getCacheInstance();
+ $cache->enableReplicas = true;
+ $cache->replicas = [
+ [
+ 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
+ 'password' => isset($redis['password']) ? $redis['password'] : null,
+ ],
+ ];
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $value);
+ $this->assertSame($cache->get($key), $value);
+
+ //Multiple Replicas listed
+ $this->resetCacheInstance();
+ $cache = $this->getCacheInstance();
+ $cache->enableReplicas = true;
+
+ $cache->replicas = [
+ [
+ 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
+ 'password' => isset($redis['password']) ? $redis['password'] : null,
+ ],
+ [
+ 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
+ 'password' => isset($redis['password']) ? $redis['password'] : null,
+ ],
+ ];
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $value);
+ $this->assertSame($cache->get($key), $value);
+
+ //invalid config
+ $this->resetCacheInstance();
+ $cache = $this->getCacheInstance();
+ $cache->enableReplicas = true;
+
+ $cache->replicas = ['redis'];
+ $this->assertFalse($cache->get($key));
+ $cache->set($key, $value);
+ $this->assertSame($cache->get($key), $value);
+
+ $this->resetCacheInstance();
+ }
+
+ public function testFlushWithSharedDatabase()
+ {
+ $instance = $this->getCacheInstance();
+ $instance->shareDatabase = true;
+ $instance->keyPrefix = 'myprefix_';
+ $instance->redis->set('testkey', 'testvalue');
+
+ for ($i = 0; $i < 1000; $i++) {
+ $instance->set(sha1($i), uniqid('', true));
+ }
+ $keys = $instance->redis->keys('*');
+ $this->assertCount(1001, $keys);
+
+ $instance->flush();
+
+ $keys = $instance->redis->keys('*');
+ $this->assertCount(1, $keys);
+ $this->assertSame(['testkey'], $keys);
+ }
+}
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
new file mode 100644
index 0000000..7185328
--- /dev/null
+++ b/tests/RedisConnectionTest.php
@@ -0,0 +1,347 @@
+getConnection(false)->configSet('timeout', 0);
+ parent::tearDown();
+ }
+
+ /**
+ * test connection to redis and selection of db
+ */
+ public function testConnect()
+ {
+ $db = $this->getConnection(false);
+ $database = $db->database;
+ $db->open();
+ $this->assertTrue($db->ping());
+ $db->set('YIITESTKEY', 'YIITESTVALUE');
+ $db->close();
+
+ $db = $this->getConnection(false);
+ $db->database = $database;
+ $db->open();
+ $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY'));
+ $db->close();
+
+ $db = $this->getConnection(false);
+ $db->database = 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()
+ {
+ $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()
+ {
+ return [
+ [123],
+ [-123],
+ [0],
+ ['test'],
+ ["test\r\ntest"],
+ [''],
+ ];
+ }
+
+ /**
+ * @dataProvider keyValueData
+ * @param mixed $data
+ */
+ public function testStoreGet($data)
+ {
+ $db = $this->getConnection(true);
+
+ $db->set('hi', $data);
+ $this->assertEquals($data, $db->get('hi'));
+ }
+
+ public function testSerialize()
+ {
+ $db = $this->getConnection(false);
+ $db->open();
+ $this->assertTrue($db->ping());
+ $s = serialize($db);
+ $this->assertTrue($db->ping());
+ $db2 = unserialize($s);
+ $this->assertTrue($db->ping());
+ $this->assertTrue($db2->ping());
+ }
+
+ public function testConnectionTimeout()
+ {
+ $db = $this->getConnection(false);
+ $db->configSet('timeout', 1);
+ $this->assertTrue($db->ping());
+ sleep(1);
+ $this->assertTrue($db->ping());
+ sleep(2);
+ if (method_exists($this, 'setExpectedException')) {
+ $this->setExpectedException('\yii\redis\SocketException');
+ } else {
+ $this->expectException('\yii\redis\SocketException');
+ }
+ $this->assertTrue($db->ping());
+ }
+
+ public function testConnectionTimeoutRetry()
+ {
+ $logger = new Logger();
+ Yii::setLogger($logger);
+
+ $db = $this->getConnection(false);
+ $db->retries = 1;
+ $db->configSet('timeout', 1);
+ $this->assertCount(3, $logger->messages, 'log of connection and init commands.');
+
+ $this->assertTrue($db->ping());
+ $this->assertCount(4, $logger->messages, 'log +1 ping command.');
+ usleep(500000); // 500ms
+
+ $this->assertTrue($db->ping());
+ $this->assertCount(5, $logger->messages, 'log +1 ping command.');
+ sleep(2);
+
+ // reconnect should happen here
+
+ $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));
+ }
+
+ public function testConnectionTimeoutRetryWithFirstFail()
+ {
+ $logger = new Logger();
+ Yii::setLogger($logger);
+
+ $databases = TestCase::getParam('databases');
+ $redis = isset($databases['redis']) ? $databases['redis'] : [];
+ $db = new ConnectionWithErrorEmulator($redis);
+ $db->retries = 3;
+
+ $db->configSet('timeout', 1);
+ $this->assertCount(3, $logger->messages, 'log of connection and init commands.');
+
+ $this->assertTrue($db->ping());
+ $this->assertCount(4, $logger->messages, 'log +1 ping command.');
+
+ sleep(2);
+
+ // Set flag for emulate socket error
+ $db->isTemporaryBroken = true;
+
+ $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));
+ }
+
+ /**
+ * Retry connecting 2 times
+ */
+ public function testConnectionTimeoutRetryCount()
+ {
+ $logger = new Logger();
+ Yii::setLogger($logger);
+
+ $db = $this->getConnection(false);
+ $db->retries = 2;
+ $db->configSet('timeout', 1);
+ $db->on(Connection::EVENT_AFTER_OPEN, function() {
+ // sleep 2 seconds after connect to make every command time out
+ sleep(2);
+ });
+ $this->assertCount(3, $logger->messages, 'log of connection and init commands.');
+
+ $exception = false;
+ try {
+ // should try to reconnect 2 times, before finally failing
+ // results in 3 times sending the PING command to redis
+ sleep(2);
+ $db->ping();
+ } catch (SocketException $e) {
+ $exception = true;
+ }
+ $this->assertTrue($exception, 'SocketException should have been thrown.');
+ $this->assertCount(14, $logger->messages, 'log +1 ping command, and reconnection.'
+ . print_r(array_map(function($s) { return (string) $s; }, ArrayHelper::getColumn($logger->messages, 0)), true));
+ }
+
+ /**
+ * https://github.com/yiisoft/yii2/issues/4745
+ */
+ public function testReturnType()
+ {
+ $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]));
+ }
+ }
+
+ public function testTwoWordCommands()
+ {
+ $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')));
+ }
+
+ /**
+ * @return array
+ */
+ public function zRangeByScoreData()
+ {
+ 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
+ */
+ public function testZRangeByScore($members, $cases)
+ {
+ $redis = $this->getConnection();
+ $set = 'zrangebyscore';
+ foreach ($members as $member) {
+ list($name, $score) = $member;
+ $this->assertEquals(1, $redis->zadd($set, $score, $name));
+ }
+
+ foreach ($cases as $case) {
+ list($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) {
+ $rows = $redis->zrangebyscore($set, $min, $max, $withScores);
+ } elseif ($limit !== null) {
+ $rows = $redis->zrangebyscore($set, $min, $max, $limit, $offset, $count);
+ } else {
+ $rows = $redis->zrangebyscore($set, $min, $max);
+ }
+ $this->assertTrue(is_array($rows));
+ $this->assertEquals(count($expectedRows), count($rows));
+ for ($i = 0; $i < count($expectedRows); $i++) {
+ $this->assertEquals($expectedRows[$i], $rows[$i]);
+ }
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function hmSetData()
+ {
+ 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
+ */
+ public function testHMSet($params, $pairs)
+ {
+ $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/RedisMutexTest.php b/tests/RedisMutexTest.php
new file mode 100644
index 0000000..7bd8a4b
--- /dev/null
+++ b/tests/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 Connection($params);
+ $this->mockApplication(['components' => ['redis' => $connection]]);
+ }
+
+ /**
+ * @return Mutex
+ * @throws \yii\base\InvalidConfigException
+ */
+ protected function createMutex()
+ {
+ return Yii::createObject([
+ 'class' => Mutex::className(),
+ '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/RedisSessionTest.php b/tests/RedisSessionTest.php
new file mode 100644
index 0000000..565dd8f
--- /dev/null
+++ b/tests/RedisSessionTest.php
@@ -0,0 +1,86 @@
+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/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..9e947f4
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,142 @@
+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 Connection($params)]]);
+
+ parent::setUp();
+ }
+
+ /**
+ * @param boolean $reset whether to clean up the test database
+ * @return Connection
+ */
+ public function getConnection($reset = true)
+ {
+ $databases = self::getParam('databases');
+ $params = isset($databases['redis']) ? $databases['redis'] : [];
+ $db = new Connection($params);
+ if ($reset) {
+ $db->open();
+ $db->flushdb();
+ }
+
+ return $db;
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ }
+
+ /**
+ * 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/UniqueValidatorTest.php b/tests/UniqueValidatorTest.php
new file mode 100644
index 0000000..345fb7f
--- /dev/null
+++ b/tests/UniqueValidatorTest.php
@@ -0,0 +1,155 @@
+getConnection(true);
+
+ $validator = new UniqueValidator();
+
+ $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->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 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/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..6b8c854
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,14 @@
+
+ * @since 2.0
+ */
+class ActiveRecord extends \yii\redis\ActiveRecord
+{
+ /**
+ * @return \yii\redis\Connection
+ */
+ public static $db;
+
+ /**
+ * @return \yii\redis\Connection
+ */
+ public static function getDb()
+ {
+ return self::$db;
+ }
+}
diff --git a/tests/data/ar/Customer.php b/tests/data/ar/Customer.php
new file mode 100644
index 0000000..e4453b9
--- /dev/null
+++ b/tests/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/data/ar/CustomerQuery.php b/tests/data/ar/CustomerQuery.php
new file mode 100644
index 0000000..e75fce5
--- /dev/null
+++ b/tests/data/ar/CustomerQuery.php
@@ -0,0 +1,21 @@
+andWhere(['status' => 1]);
+
+ return $this;
+ }
+}
diff --git a/tests/data/ar/Item.php b/tests/data/ar/Item.php
new file mode 100644
index 0000000..135d9c1
--- /dev/null
+++ b/tests/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/data/ar/OrderItem.php b/tests/data/ar/OrderItem.php
new file mode 100644
index 0000000..26c1353
--- /dev/null
+++ b/tests/data/ar/OrderItem.php
@@ -0,0 +1,49 @@
+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/data/ar/OrderItemWithNullFK.php b/tests/data/ar/OrderItemWithNullFK.php
new file mode 100644
index 0000000..7bb2201
--- /dev/null
+++ b/tests/data/ar/OrderItemWithNullFK.php
@@ -0,0 +1,30 @@
+ [
+ 'redis' => [
+ 'hostname' => 'localhost',
+ 'port' => 6379,
+ 'database' => 0,
+ 'password' => null,
+ ],
+ ],
+];
+
+if (is_file(__DIR__ . '/config.local.php')) {
+ include(__DIR__ . '/config.local.php');
+}
+
+return $config;
\ No newline at end of file
diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml
new file mode 100644
index 0000000..b8aecf7
--- /dev/null
+++ b/tests/docker/docker-compose.yml
@@ -0,0 +1,24 @@
+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"
+ networks:
+ - yii2-redis
+ volumes:
+ - ../..:/app # Mount source-code for development
+
+ Redis:
+ image: "redis"
+ networks:
+ - yii2-redis
+ ports:
+ - "6379:6379"
+
+networks:
+ yii2-redis:
+ driver: bridge
+ name: yii2-redis
diff --git a/tests/support/ConnectionWithErrorEmulator.php b/tests/support/ConnectionWithErrorEmulator.php
new file mode 100644
index 0000000..6ab999b
--- /dev/null
+++ b/tests/support/ConnectionWithErrorEmulator.php
@@ -0,0 +1,20 @@
+isTemporaryBroken) {
+ // Unset flag for emulate socket error
+ $this->isTemporaryBroken = false;
+ throw new SocketException("Failed to read from socket.\nRedis command was: " . $command);
+ }
+ return parent::sendRawCommand($command, $params);
+ }
+}
From e7c45d7e5a4355ce6d0cf4773b57e6874e35387a Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:10:54 -0300
Subject: [PATCH 02/25] Fix test.
---
tests/ActiveRecordTest.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index 0b7fa46..c3bb984 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -6,11 +6,12 @@
use yii\redis\LuaScriptBuilder;
use yiiunit\extensions\redis\data\ar\ActiveRecord;
use yiiunit\extensions\redis\data\ar\Customer;
-use yiiunit\extensions\redis\data\ar\OrderItem;
-use yiiunit\extensions\redis\data\ar\Order;
use yiiunit\extensions\redis\data\ar\Item;
+use yiiunit\extensions\redis\data\ar\Order;
+use yiiunit\extensions\redis\data\ar\OrderItem;
use yiiunit\extensions\redis\data\ar\OrderItemWithNullFK;
use yiiunit\extensions\redis\data\ar\OrderWithNullFK;
+use yiiunit\framework\ar\ActiveRecordTestTrait;
/**
* @group redis
From 3270c30f1f5d2f6feb4b31f38f4807046df80c06 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:13:05 -0300
Subject: [PATCH 03/25] Fix tests 1.
---
phpunit.xml.dist | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f29a28d..1d31f97 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -2,7 +2,7 @@
Date: Tue, 17 Oct 2023 14:16:49 -0300
Subject: [PATCH 04/25] fix test 2.
---
composer.json | 2 +-
infection.json.dist | 16 ----------------
phpunit.xml.dist | 2 +-
tests/RedisConnectionTest.php | 1 +
4 files changed, 3 insertions(+), 18 deletions(-)
delete mode 100644 infection.json.dist
diff --git a/composer.json b/composer.json
index 02cdf2f..2ee1713 100644
--- a/composer.json
+++ b/composer.json
@@ -29,7 +29,7 @@
},
"autoload-dev": {
"psr-4": {
- "yiiunit\\extensions\\redis\\": "tests/"
+ "yiiunit\\extensions\\redis\\": "tests"
}
},
"extra": {
diff --git a/infection.json.dist b/infection.json.dist
deleted file mode 100644
index d06b334..0000000
--- a/infection.json.dist
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "source": {
- "directories": [
- "src"
- ]
- },
- "logs": {
- "text": "php:\/\/stderr",
- "stryker": {
- "report": "main"
- }
- },
- "mutators": {
- "@default": true
- }
-}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 1d31f97..e7ebb21 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,7 +11,7 @@
stopOnFailure="false"
>
-
+
tests
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index 7185328..0028f72 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -1,6 +1,7 @@
Date: Tue, 17 Oct 2023 14:20:14 -0300
Subject: [PATCH 05/25] Use `Yii::debug()`.
---
src/Connection.php | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Connection.php b/src/Connection.php
index fd8e63c..036057d 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -631,7 +631,7 @@ public function open()
}
$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,
@@ -672,7 +672,7 @@ public function close()
{
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 +769,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) {
From fb63b34235530a219edfea9889934be81ba37469 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:23:25 -0300
Subject: [PATCH 06/25] Fix tests 3.
---
composer.json | 2 ++
tests/ActiveRecordTest.php | 8 ++++----
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/composer.json b/composer.json
index 2ee1713..6bf9d90 100644
--- a/composer.json
+++ b/composer.json
@@ -14,6 +14,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.1",
+ "ext-ctype": "*",
+ "ext-mbstring": "*",
"yiisoft/yii2": "^2.2"
},
"require-dev": {
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index c3bb984..c1f4872 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -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();
From 31064f51fe46490e3445f1951f81db6b8b461680 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:25:25 -0300
Subject: [PATCH 07/25] fix tests 5.
---
tests/RedisConnectionTest.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index 0028f72..a929641 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -12,7 +12,7 @@
/**
* @group redis
*/
-class ConnectionTest extends TestCase
+class RedisConnectionTest extends TestCase
{
protected function tearDown(): void
{
From c1fd4bd614cbabfbaf4e88a030693696df799dab Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 14:33:24 -0300
Subject: [PATCH 08/25] Fix tests 6.
---
composer-require-checker.json | 5 +
composer.json | 3 +-
src/ActiveQuery.php | 6 +-
src/LuaScriptBuilder.php | 4 +-
tests/ActiveRecordTest.php | 2 +-
tests/data/ar/ActiveRecordTestTrait.php | 1314 +++++++++++++++++++++++
6 files changed, 1326 insertions(+), 8 deletions(-)
create mode 100644 composer-require-checker.json
create mode 100644 tests/data/ar/ActiveRecordTestTrait.php
diff --git a/composer-require-checker.json b/composer-require-checker.json
new file mode 100644
index 0000000..8a8e38c
--- /dev/null
+++ b/composer-require-checker.json
@@ -0,0 +1,5 @@
+{
+ "symbol-whitelist": [
+ "YII_DEBUG"
+ ]
+}
diff --git a/composer.json b/composer.json
index 6bf9d90..0bb8eb5 100644
--- a/composer.json
+++ b/composer.json
@@ -21,8 +21,7 @@
"require-dev": {
"maglnet/composer-require-checker": "^4.6",
"phpunit/phpunit": "^10.2",
- "yii2-extensions/phpstan": "dev-main",
- "yiisoft/yii2-dev": "^2.2"
+ "yii2-extensions/phpstan": "dev-main"
},
"autoload": {
"psr-4": {
diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php
index ddb321d..aa41b49 100644
--- a/src/ActiveQuery.php
+++ b/src/ActiveQuery.php
@@ -8,7 +8,7 @@
namespace yii\redis;
use yii\base\Component;
-use yii\base\InvalidParamException;
+use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
@@ -420,7 +420,7 @@ protected function executeScript($db, $type, $columnName = null)
* @param string $type the type of the script to generate
* @param string $columnName
* @return array|bool|null|string
- * @throws \yii\base\InvalidParamException
+ * @throws \yii\base\InvalidArgumentException
* @throws \yii\base\NotSupportedException
*/
private function findByPk($db, $type, $columnName = null)
@@ -574,6 +574,6 @@ private function findByPk($db, $type, $columnName = null)
return $max;
}
- throw new InvalidParamException('Unknown fetch type: ' . $type);
+ throw new InvalidArgumentException('Unknown fetch type: ' . $type);
}
}
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index 489c20b..1be5830 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -7,7 +7,7 @@
namespace yii\redis;
-use yii\base\InvalidParamException;
+use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
use yii\db\Exception;
use yii\db\Expression;
@@ -317,7 +317,7 @@ private function buildHashCondition($condition, &$columns)
private function buildNotCondition($operator, $operands, &$params)
{
if (count($operands) != 1) {
- throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
+ throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
}
$operand = reset($operands);
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index c1f4872..c312951 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -5,13 +5,13 @@
use yii\redis\ActiveQuery;
use yii\redis\LuaScriptBuilder;
use yiiunit\extensions\redis\data\ar\ActiveRecord;
+use yiiunit\extensions\redis\data\ar\ActiveRecordTestTrait;
use yiiunit\extensions\redis\data\ar\Customer;
use yiiunit\extensions\redis\data\ar\Item;
use yiiunit\extensions\redis\data\ar\Order;
use yiiunit\extensions\redis\data\ar\OrderItem;
use yiiunit\extensions\redis\data\ar\OrderItemWithNullFK;
use yiiunit\extensions\redis\data\ar\OrderWithNullFK;
-use yiiunit\framework\ar\ActiveRecordTestTrait;
/**
* @group redis
diff --git a/tests/data/ar/ActiveRecordTestTrait.php b/tests/data/ar/ActiveRecordTestTrait.php
new file mode 100644
index 0000000..a260edf
--- /dev/null
+++ b/tests/data/ar/ActiveRecordTestTrait.php
@@ -0,0 +1,1314 @@
+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 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::class, 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::class, BaseActiveRecord::EVENT_AFTER_FIND);
+ }
+
+ public function testAfterRefresh()
+ {
+ /* @var $customerClass ActiveRecordInterface */
+ $customerClass = $this->getCustomerClass();
+ /* @var $this TestCase|ActiveRecordTestTrait */
+
+ $afterRefreshCalls = [];
+ Event::on(BaseActiveRecord::class, 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::class, 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);
+ }
+}
From 1eac1f92e67a8c3e1833d9c8bdcc4d67b4f85cfe Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 16:51:06 -0300
Subject: [PATCH 09/25] Fix tests 7.
---
tests/CacheTestCase.php | 275 +++++++++++++++++++++++++++++++++++++++
tests/RedisCacheTest.php | 1 -
2 files changed, 275 insertions(+), 1 deletion(-)
create mode 100644 tests/CacheTestCase.php
diff --git a/tests/CacheTestCase.php b/tests/CacheTestCase.php
new file mode 100644
index 0000000..92b055d
--- /dev/null
+++ b/tests/CacheTestCase.php
@@ -0,0 +1,275 @@
+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 static function multiSetExpiry(): array
+ {
+ return [[0], [2]];
+ }
+
+ /**
+ * @dataProvider multiSetExpiry
+ *
+ * @param int $expiry Expiry in seconds.
+ */
+ public function testMultiset(int $expiry): void
+ {
+ $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/RedisCacheTest.php b/tests/RedisCacheTest.php
index 548727a..4008bd2 100644
--- a/tests/RedisCacheTest.php
+++ b/tests/RedisCacheTest.php
@@ -4,7 +4,6 @@
use yii\redis\Cache;
use yii\redis\Connection;
-use yiiunit\framework\caching\CacheTestCase;
/**
* Class for testing redis cache backend
From cfd654aa2a5912badfe99f2b1f407484b542ef4d Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:03:11 -0300
Subject: [PATCH 10/25] Fix dataproviders.
---
tests/ActiveRecordTest.php | 56 +------------
tests/CacheTestCase.php | 10 +--
tests/RedisConnectionTest.php | 87 +-------------------
tests/RedisMutexTest.php | 13 +--
tests/providers/Data.php | 144 ++++++++++++++++++++++++++++++++++
5 files changed, 152 insertions(+), 158 deletions(-)
create mode 100644 tests/providers/Data.php
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index c312951..ca71a8d 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -509,25 +509,8 @@ public function testCountQuery()
$this->assertEquals(2, $query->count());
}
- public function illegalValuesForWhere()
- {
- 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
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::illegalValuesForWhere
*/
public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $unexpectedStrings = [])
{
@@ -546,43 +529,8 @@ public function testValueEscapingInWhere($filterWithInjection, $expectedStrings,
}
}
- public function illegalValuesForFindByCondition()
- {
- 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
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::illegalValuesForFindByCondition
*/
public function testValueEscapingInFindByCondition($filterWithInjection, $expectedStrings, $unexpectedStrings = [])
{
diff --git a/tests/CacheTestCase.php b/tests/CacheTestCase.php
index 92b055d..832298b 100644
--- a/tests/CacheTestCase.php
+++ b/tests/CacheTestCase.php
@@ -77,15 +77,7 @@ public function testGet()
}
/**
- * @return array testing multiSet with and without expiry
- */
- public static function multiSetExpiry(): array
- {
- return [[0], [2]];
- }
-
- /**
- * @dataProvider multiSetExpiry
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::multiSetExpiry
*
* @param int $expiry Expiry in seconds.
*/
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index a929641..e73886f 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -60,24 +60,8 @@ public function testReConnect()
$db->close();
}
-
- /**
- * @return array
- */
- public function keyValueData()
- {
- return [
- [123],
- [-123],
- [0],
- ['test'],
- ["test\r\ntest"],
- [''],
- ];
- }
-
/**
- * @dataProvider keyValueData
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::keyValueData
* @param mixed $data
*/
public function testStoreGet($data)
@@ -233,47 +217,7 @@ public function testTwoWordCommands()
}
/**
- * @return array
- */
- public function zRangeByScoreData()
- {
- 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
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::zRangeByScoreData
* @param array $members
* @param array $cases
*/
@@ -306,32 +250,7 @@ public function testZRangeByScore($members, $cases)
}
/**
- * @return array
- */
- public function hmSetData()
- {
- 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
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::hmSetData
* @param array $params
* @param array $pairs
*/
diff --git a/tests/RedisMutexTest.php b/tests/RedisMutexTest.php
index 7bd8a4b..161f8d2 100644
--- a/tests/RedisMutexTest.php
+++ b/tests/RedisMutexTest.php
@@ -51,20 +51,11 @@ public function testExpiration()
$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
+ *
+ * @dataProvider \yiiunit\extensions\redis\providers\Data::acquireTimeout
*/
public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTimeout, $lockIsReleased)
{
diff --git a/tests/providers/Data.php b/tests/providers/Data.php
new file mode 100644
index 0000000..e48486f
--- /dev/null
+++ b/tests/providers/Data.php
@@ -0,0 +1,144 @@
+ [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],
+ ];
+ }
+
+ public static 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'
+ ],
+ ]
+ ];
+ }
+
+ public static 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') .. '"]],
+ ];
+ }
+
+ public static 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=']],
+ ];
+ }
+
+ public static function keyValueData(): array
+ {
+ return [
+ [123],
+ [-123],
+ [0],
+ ['test'],
+ ["test\r\ntest"],
+ [''],
+ ];
+ }
+
+ public static function multiSetExpiry(): array
+ {
+ return [[0], [2]];
+ }
+
+ public static 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]],
+ ],
+ ],
+ ];
+ }
+}
From 29cfcbbb97bdb69439e3619230626f19b3d2db60 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:06:31 -0300
Subject: [PATCH 11/25] Show deprecations.
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 85505ce..923a9c3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,7 +51,7 @@ jobs:
- name: Run tests with phpunit and generate coverage.
if: matrix.php == '8.1'
- run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
+ run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always --display-deprecations
- name: Upload coverage to Codecov.
if: matrix.php == '8.1'
From 11cbf054a09d864620eeb0f83ecfb742b8c2e545 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:11:25 -0300
Subject: [PATCH 12/25] Fix deprecations.
---
.github/workflows/build.yml | 2 +-
src/Connection.php | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 923a9c3..85505ce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,7 +51,7 @@ jobs:
- name: Run tests with phpunit and generate coverage.
if: matrix.php == '8.1'
- run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always --display-deprecations
+ run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
- name: Upload coverage to Codecov.
if: matrix.php == '8.1'
diff --git a/src/Connection.php b/src/Connection.php
index 036057d..d9b24cd 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -765,11 +765,13 @@ public function executeCommand($name, $params = [])
$params = array_merge(explode(' ', $name), $params);
$command = '*' . count($params) . "\r\n";
+
foreach ($params as $arg) {
- $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
+ $command .= '$' . mb_strlen((string) $arg, '8bit') . "\r\n" . (string) $arg . "\r\n";
}
\Yii::debug("Executing Redis Command: {$name}", __METHOD__);
+
if ($this->retries > 0) {
$tries = $this->retries;
while ($tries-- > 0) {
From 16dbbc7e16ccff2e2b855222b047e7b768f00207 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:24:58 -0300
Subject: [PATCH 13/25] More rework code.
---
src/ActiveQuery.php | 10 ++--------
src/ActiveRecord.php | 10 ++--------
src/Cache.php | 17 ++---------------
src/Connection.php | 20 ++------------------
src/LuaScriptBuilder.php | 10 ++--------
src/Mutex.php | 11 ++---------
src/Session.php | 10 ++--------
src/SocketException.php | 8 ++------
tests/ActiveDataProviderTest.php | 2 ++
tests/ActiveRecordTest.php | 2 ++
tests/CacheTestCase.php | 2 ++
tests/RedisCacheTest.php | 4 +++-
tests/RedisConnectionTest.php | 4 +++-
tests/RedisMutexTest.php | 2 ++
tests/RedisSessionTest.php | 2 ++
tests/TestCase.php | 2 ++
tests/UniqueValidatorTest.php | 2 ++
tests/bootstrap.php | 2 ++
tests/data/ar/ActiveRecord.php | 8 ++------
tests/data/ar/ActiveRecordTestTrait.php | 8 ++------
tests/data/ar/Customer.php | 2 ++
tests/data/ar/CustomerQuery.php | 2 ++
tests/data/ar/Item.php | 2 ++
tests/data/ar/Order.php | 2 ++
tests/data/ar/OrderItem.php | 2 ++
tests/data/ar/OrderItemWithNullFK.php | 2 ++
tests/data/ar/OrderWithNullFK.php | 2 ++
tests/data/config.php | 6 ++++--
tests/providers/Data.php | 2 ++
29 files changed, 62 insertions(+), 96 deletions(-)
diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php
index aa41b49..0fb011d 100644
--- a/src/ActiveQuery.php
+++ b/src/ActiveQuery.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class ActiveQuery extends Component implements ActiveQueryInterface
{
diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php
index 9258ddd..eab8949 100644
--- a/src/ActiveRecord.php
+++ b/src/ActiveRecord.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class ActiveRecord extends BaseActiveRecord
{
diff --git a/src/Cache.php b/src/Cache.php
index d9a4ad7..d678d06 100644
--- a/src/Cache.php
+++ b/src/Cache.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class Cache extends \yii\caching\Cache
{
@@ -110,7 +104,6 @@ class Cache extends \yii\caching\Cache
public $redis = 'redis';
/**
* @var bool whether to enable read / get from redis replicas.
- * @since 2.0.8
* @see $replicas
*/
public $enableReplicas = false;
@@ -128,8 +121,6 @@ class Cache extends \yii\caching\Cache
* ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
* ],
* ```
- *
- * @since 2.0.8
* @see $enableReplicas
*/
public $replicas = [];
@@ -137,7 +128,6 @@ class Cache extends \yii\caching\Cache
* @var bool|null force cluster mode, don't check on every request. If this is null, cluster mode will be checked
* once per request whenever the cache is accessed. To disable the check, set to true if cluster mode
* should be enabled, or false if it should be disabled.
- * @since 2.0.11
*/
public $forceClusterMode;
/**
@@ -146,7 +136,6 @@ class Cache extends \yii\caching\Cache
* command, component will iterate through all keys in database and remove only these with matching [[$keyPrefix]].
* Note that this will no longer be an atomic operation and it is much less efficient than `FLUSHDB` command. It is
* recommended to use separate database for cache and leave this value as `false`.
- * @since 2.0.12
*/
public $shareDatabase = false;
@@ -300,7 +289,6 @@ protected function setValues($data, $expire)
*
* Setting [[forceClusterMode]] to either `true` or `false` is preferred.
* @return bool whether redis is running in cluster mode or not
- * @since 2.0.11
*/
public function getIsCluster()
{
@@ -370,7 +358,6 @@ 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
* @throws \yii\base\InvalidConfigException
*/
diff --git a/src/Connection.php b/src/Connection.php
index d9b24cd..3f678dd 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class Connection extends Component
{
@@ -261,12 +255,10 @@ class Connection extends Component
public $hostname = 'localhost';
/**
* @var string the connection scheme used for connecting to the redis server. Defaults to 'tcp'.
- * @since 2.0.18
*/
public $scheme = 'tcp';
/**
* @var string if the query gets redirected, use this as the temporary new hostname
- * @since 2.0.11
*/
public $redirectConnectionString;
/**
@@ -278,7 +270,6 @@ class Connection extends Component
* @var string the unix socket path (e.g. `/var/run/redis/redis.sock`) to use for connecting to the redis server.
* This can be used instead of [[hostname]] and [[port]] to connect to the server using a unix socket.
* If a unix socket path is specified, [[hostname]] and [[port]] will be ignored.
- * @since 2.0.1
*/
public $unixSocket;
/**
@@ -286,7 +277,6 @@ class Connection extends Component
* Username was introduced in Redis 6.
* @link https://redis.io/commands/auth
* @link https://redis.io/topics/acl
- * @since 2.0.16
*/
public $username;
/**
@@ -309,13 +299,11 @@ class Connection extends Component
public $dataTimeout;
/**
* @var boolean Send sockets over SSL protocol. Default state is false.
- * @since 2.0.12
*/
public $useSSL = false;
/**
* @var array PHP context options which are used in the Redis connection stream.
* @see https://www.php.net/manual/en/context.ssl.php
- * @since 2.0.15
*/
public $contextOptions = [];
/**
@@ -335,21 +323,18 @@ class Connection extends Component
* > ```
*
* @see https://www.php.net/manual/en/function.stream-socket-client.php
- * @since 2.0.5
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT;
/**
* @var integer The number of times a command execution should be retried when a connection failure occurs.
* This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
* Defaults to 0 meaning no retries on failure.
- * @since 2.0.7
*/
public $retries = 0;
/**
* @var integer The retry interval in microseconds to wait between retry.
* This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
* Defaults to 0 meaning no wait.
- * @since 2.0.10
*/
public $retryInterval = 0;
/**
@@ -590,7 +575,6 @@ public function __sleep()
* Return the connection string used to open a socket connection. During a redirect (cluster mode) this will be the
* target of the redirect.
* @return string socket connection string
- * @since 2.0.11
*/
public function getConnectionString()
{
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index 1be5830..f0bd9e8 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class LuaScriptBuilder extends \yii\base\BaseObject
{
diff --git a/src/Mutex.php b/src/Mutex.php
index 78a8d6c..9158dec 100644
--- a/src/Mutex.php
+++ b/src/Mutex.php
@@ -1,9 +1,6 @@
- * @author Alexander Zhuravlev
- * @since 2.0.6
*/
class Mutex extends \yii\mutex\Mutex
{
diff --git a/src/Session.php b/src/Session.php
index 893b27d..b318c04 100644
--- a/src/Session.php
+++ b/src/Session.php
@@ -1,9 +1,6 @@
- * @since 2.0
*/
class Session extends \yii\web\Session
{
diff --git a/src/SocketException.php b/src/SocketException.php
index d286ad7..5be5bf2 100644
--- a/src/SocketException.php
+++ b/src/SocketException.php
@@ -1,9 +1,6 @@
redis->set('testkey', 'testvalue');
for ($i = 0; $i < 1000; $i++) {
- $instance->set(sha1($i), uniqid('', true));
+ $instance->set(sha1((string) $i), uniqid('', true));
}
$keys = $instance->redis->keys('*');
$this->assertCount(1001, $keys);
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index e73886f..585530d 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -1,5 +1,7 @@
assertTrue($db->ping());
sleep(2);
if (method_exists($this, 'setExpectedException')) {
- $this->setExpectedException('\yii\redis\SocketException');
+ $this->expectException('\yii\redis\SocketException');
} else {
$this->expectException('\yii\redis\SocketException');
}
diff --git a/tests/RedisMutexTest.php b/tests/RedisMutexTest.php
index 161f8d2..31fcc94 100644
--- a/tests/RedisMutexTest.php
+++ b/tests/RedisMutexTest.php
@@ -1,5 +1,7 @@
- * @since 2.0
- */
class ActiveRecord extends \yii\redis\ActiveRecord
{
/**
diff --git a/tests/data/ar/ActiveRecordTestTrait.php b/tests/data/ar/ActiveRecordTestTrait.php
index a260edf..c6d6fa6 100644
--- a/tests/data/ar/ActiveRecordTestTrait.php
+++ b/tests/data/ar/ActiveRecordTestTrait.php
@@ -1,12 +1,8 @@
[
'redis' => [
@@ -26,4 +28,4 @@
include(__DIR__ . '/config.local.php');
}
-return $config;
\ No newline at end of file
+return $config;
diff --git a/tests/providers/Data.php b/tests/providers/Data.php
index e48486f..2c98ffc 100644
--- a/tests/providers/Data.php
+++ b/tests/providers/Data.php
@@ -1,5 +1,7 @@
Date: Tue, 17 Oct 2023 20:25:16 +0000
Subject: [PATCH 14/25] Apply fixes from StyleCI
---
src/ActiveQuery.php | 42 ++++++++++---
src/ActiveRecord.php | 12 +++-
src/Cache.php | 18 ++++--
src/Connection.php | 61 ++++++++++++-------
src/LuaScriptBuilder.php | 57 ++++++++++++-----
src/Mutex.php | 10 ++-
src/Session.php | 18 +++++-
tests/ActiveRecordTest.php | 16 +++--
tests/CacheTestCase.php | 8 ++-
tests/RedisCacheTest.php | 21 ++++---
tests/RedisConnectionTest.php | 31 ++++++----
tests/RedisMutexTest.php | 8 ++-
tests/RedisSessionTest.php | 4 +-
tests/TestCase.php | 17 ++++--
tests/UniqueValidatorTest.php | 1 -
tests/data/ar/ActiveRecordTestTrait.php | 5 ++
tests/data/ar/Customer.php | 7 ++-
tests/data/ar/Order.php | 17 +++---
tests/data/ar/OrderItem.php | 1 -
tests/data/ar/OrderItemWithNullFK.php | 6 +-
tests/data/ar/OrderWithNullFK.php | 6 +-
tests/providers/Data.php | 16 ++---
tests/support/ConnectionWithErrorEmulator.php | 6 +-
23 files changed, 257 insertions(+), 131 deletions(-)
diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php
index 0fb011d..db76011 100644
--- a/src/ActiveQuery.php
+++ b/src/ActiveQuery.php
@@ -67,18 +67,18 @@
*/
class ActiveQuery extends Component implements ActiveQueryInterface
{
- use QueryTrait;
use ActiveQueryTrait;
use ActiveRelationTrait;
+ use QueryTrait;
/**
* @event Event an event that is triggered when the query is initialized via [[init()]].
*/
- const EVENT_INIT = 'init';
-
+ public const EVENT_INIT = 'init';
/**
* Constructor.
+ *
* @param string $modelClass the model class associated with this query
* @param array $config configurations to be applied to the newly created query object
*/
@@ -102,9 +102,11 @@ public function init()
/**
* Executes the query and returns all results as an array.
+ *
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
- * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
+ *
+ * @return ActiveRecord[]|array the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null)
{
@@ -144,7 +146,7 @@ public function all($db = null)
}
} else {
foreach ($models as $model) {
- $key = call_user_func($this->indexBy, $model);
+ $key = ($this->indexBy)($model);
$indexedModels[$key] = $model;
}
}
@@ -161,8 +163,10 @@ public function all($db = null)
/**
* Executes the query and returns a single row of result.
+ *
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
* the query result may be either an array or an ActiveRecord object. Null will be returned
* if the query results in nothing.
@@ -206,9 +210,11 @@ public function one($db = null)
/**
* Returns the number of records.
+ *
* @param string $q the COUNT expression. This parameter is ignored by this implementation.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return int number of records
*/
public function count($q = '*', $db = null)
@@ -232,8 +238,10 @@ public function count($q = '*', $db = null)
/**
* Returns a value indicating whether the query result contains any row of data.
+ *
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return bool whether the query result contains any row of data.
*/
public function exists($db = null)
@@ -246,9 +254,11 @@ public function exists($db = null)
/**
* Executes the query and returns the first column of the result.
+ *
* @param string $column name of the column to select
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return array the first column of the query result. An empty array is returned if the query results in nothing.
*/
public function column($column, $db = null)
@@ -263,9 +273,11 @@ public function column($column, $db = null)
/**
* Returns the number of records.
+ *
* @param string $column the column to sum up
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return int number of records
*/
public function sum($column, $db = null)
@@ -279,10 +291,12 @@ public function sum($column, $db = null)
/**
* Returns the average of the specified column values.
+ *
* @param string $column the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return int the average of the specified column values.
*/
public function average($column, $db = null)
@@ -295,10 +309,12 @@ public function average($column, $db = null)
/**
* Returns the minimum of the specified column values.
+ *
* @param string $column the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return int the minimum of the specified column values.
*/
public function min($column, $db = null)
@@ -311,10 +327,12 @@ public function min($column, $db = null)
/**
* Returns the maximum of the specified column values.
+ *
* @param string $column the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return int the maximum of the specified column values.
*/
public function max($column, $db = null)
@@ -328,9 +346,11 @@ public function max($column, $db = null)
/**
* Returns the query result as a scalar value.
* The value returned will be the specified attribute in the first record of the query results.
+ *
* @param string $attribute name of the attribute to select
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
+ *
* @return string the value of the specified attribute in the first record of the query result.
* Null is returned if the query result is empty.
*/
@@ -350,12 +370,15 @@ public function scalar($attribute, $db = null)
/**
* Executes a script created by [[LuaScriptBuilder]]
+ *
* @param Connection|null $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @param string $type the type of the script to generate
* @param string $columnName
+ *
* @throws NotSupportedException
- * @return array|bool|null|string
+ *
+ * @return array|bool|string|null
*/
protected function executeScript($db, $type, $columnName = null)
{
@@ -368,7 +391,7 @@ protected function executeScript($db, $type, $columnName = null)
} elseif (is_array($this->via)) {
// via relation
/* @var $viaQuery ActiveQuery */
- list($viaName, $viaQuery) = $this->via;
+ [$viaName, $viaQuery] = $this->via;
if ($viaQuery->multiple) {
$viaModels = $viaQuery->all();
$this->primaryModel->populateRelation($viaName, $viaModels);
@@ -409,13 +432,16 @@ protected function executeScript($db, $type, $columnName = null)
/**
* Fetch by pk if possible as this is much faster
+ *
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @param string $type the type of the script to generate
* @param string $columnName
- * @return array|bool|null|string
+ *
* @throws \yii\base\InvalidArgumentException
* @throws \yii\base\NotSupportedException
+ *
+ * @return array|bool|string|null
*/
private function findByPk($db, $type, $columnName = null)
{
diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php
index eab8949..3590641 100644
--- a/src/ActiveRecord.php
+++ b/src/ActiveRecord.php
@@ -36,6 +36,7 @@ class ActiveRecord extends BaseActiveRecord
* Returns the database connection used by this AR class.
* By default, the "redis" application component is used as the database connection.
* You may override this method if you want to use a different database connection.
+ *
* @return Connection the database connection used by this AR class.
*/
public static function getDb()
@@ -45,11 +46,12 @@ public static function getDb()
/**
* @inheritdoc
+ *
* @return ActiveQuery the newly created [[ActiveQuery]] instance.
*/
public static function find()
{
- return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
+ return Yii::createObject(ActiveQuery::className(), [static::class]);
}
/**
@@ -68,6 +70,7 @@ public static function primaryKey()
/**
* Returns the list of all attribute names of the model.
* This method must be overridden by child classes to define available attributes.
+ *
* @return array list of attribute names.
*/
public function attributes()
@@ -80,11 +83,12 @@ public function attributes()
* By default this method returns the class name as the table name by calling [[Inflector::camel2id()]].
* For example, 'Customer' becomes 'customer', and 'OrderItem' becomes
* 'order_item'. You may override this method if you want different key naming.
+ *
* @return string the prefix to apply to all AR keys
*/
public static function keyPrefix()
{
- return Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
+ return Inflector::camel2id(StringHelper::basename(static::class), '_');
}
/**
@@ -155,6 +159,7 @@ public function insert($runValidation = true, $attributes = null)
* @param array $attributes attribute values (name-value pairs) to be saved into the table
* @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ *
* @return int the number of rows updated
*/
public static function updateAll($attributes, $condition = null)
@@ -226,6 +231,7 @@ public static function updateAll($attributes, $condition = null)
* Use negative values if you want to decrement the counters.
* @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ *
* @return int the number of rows updated
*/
public static function updateAllCounters($counters, $condition = null)
@@ -258,6 +264,7 @@ public static function updateAllCounters($counters, $condition = null)
*
* @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ *
* @return int the number of rows deleted
*/
public static function deleteAll($condition = null)
@@ -304,6 +311,7 @@ private static function fetchPks($condition)
* Builds a normalized key from a given primary key value.
*
* @param mixed $key the key to be normalized
+ *
* @return string the generated key
*/
public static function buildKey($key)
diff --git a/src/Cache.php b/src/Cache.php
index d678d06..f3315a8 100644
--- a/src/Cache.php
+++ b/src/Cache.php
@@ -90,12 +90,12 @@
* ]);
* ~~~
*
- * @property-read bool $isCluster Whether redis is running in cluster mode or not.
+ * @property bool $isCluster Whether redis is running in cluster mode or not.
*/
class Cache extends \yii\caching\Cache
{
/**
- * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]].
+ * @var array|Connection|string 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
@@ -104,6 +104,7 @@ class Cache extends \yii\caching\Cache
public $redis = 'redis';
/**
* @var bool whether to enable read / get from redis replicas.
+ *
* @see $replicas
*/
public $enableReplicas = false;
@@ -121,6 +122,7 @@ class Cache extends \yii\caching\Cache
* ['hostname' => 'redis-slave-003.xyz.0001.apse1.cache.amazonaws.com'],
* ],
* ```
+ *
* @see $enableReplicas
*/
public $replicas = [];
@@ -152,10 +154,10 @@ class Cache extends \yii\caching\Cache
*/
private $_hashTagAvailable = false;
-
/**
* Initializes the redis Cache component.
* This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ *
* @throws \yii\base\InvalidConfigException if [[redis]] is invalid.
*/
public function init()
@@ -170,8 +172,10 @@ public function init()
* Note that this method does not check whether the dependency associated
* with the cached data, if there is any, has changed. So a call to [[get]]
* may return false while exists returns true.
+ *
* @param mixed $key a key identifying the cached value. This can be a simple string or
* a complex data structure consisting of factors representing the key.
+ *
* @return bool true if a value exists in cache, false if the value is not in the cache or expired.
*/
public function exists($key)
@@ -219,7 +223,6 @@ public function buildKey($key)
is_string($key)
&& $this->isCluster
&& preg_match('/^(.*)({.+})(.*)$/', $key, $matches) === 1) {
-
$this->_hashTagAvailable = true;
return parent::buildKey($matches[1] . $matches[3]) . $matches[2];
@@ -288,6 +291,7 @@ protected function setValues($data, $expire)
* `CLUSTER INFO` executes successfully, `false` otherwise.
*
* Setting [[forceClusterMode]] to either `true` or `false` is preferred.
+ *
* @return bool whether redis is running in cluster mode or not
*/
public function getIsCluster()
@@ -342,7 +346,7 @@ protected function flushValues()
if ($this->shareDatabase) {
$cursor = 0;
do {
- list($cursor, $keys) = $this->redis->scan($cursor, 'MATCH', $this->keyPrefix . '*');
+ [$cursor, $keys] = $this->redis->scan($cursor, 'MATCH', $this->keyPrefix . '*');
$cursor = (int) $cursor;
if (!empty($keys)) {
$this->redis->executeCommand('DEL', $keys);
@@ -358,8 +362,10 @@ 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().
- * @return array|string|Connection
+ *
* @throws \yii\base\InvalidConfigException
+ *
+ * @return array|Connection|string
*/
protected function getReplica()
{
diff --git a/src/Connection.php b/src/Connection.php
index 3f678dd..7e0fbd8 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -235,18 +235,18 @@
* @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.
- * @property-read LuaScriptBuilder $luaScriptBuilder
- * @property-read resource|false $socket
+ * @property string $connectionString Socket connection string.
+ * @property string $driverName Name of the DB driver.
+ * @property bool $isActive Whether the DB connection is established.
+ * @property LuaScriptBuilder $luaScriptBuilder
+ * @property false|resource $socket
*/
class Connection extends Component
{
/**
* @event Event an event that is triggered after a DB connection is established
*/
- const EVENT_AFTER_OPEN = 'afterOpen';
+ public const EVENT_AFTER_OPEN = 'afterOpen';
/**
* @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'.
@@ -262,7 +262,7 @@ class Connection extends Component
*/
public $redirectConnectionString;
/**
- * @var integer the port to use for connecting to the redis server. Default port is 6379.
+ * @var int the port to use for connecting to the redis server. Default port is 6379.
* If [[unixSocket]] is specified, [[hostname]] and port will be ignored.
*/
public $port = 6379;
@@ -275,6 +275,7 @@ class Connection extends Component
/**
* @var string|null username for establishing DB connection. Defaults to `null` meaning AUTH command will be performed without username.
* Username was introduced in Redis 6.
+ *
* @link https://redis.io/commands/auth
* @link https://redis.io/topics/acl
*/
@@ -285,7 +286,7 @@ class Connection extends Component
*/
public $password;
/**
- * @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0.
+ * @var int the redis database to use. This is an integer value starting from 0. Defaults to 0.
* Since version 2.0.6 you can disable the SELECT command sent after connection by setting this property to `null`.
*/
public $database = 0;
@@ -298,16 +299,17 @@ class Connection extends Component
*/
public $dataTimeout;
/**
- * @var boolean Send sockets over SSL protocol. Default state is false.
+ * @var bool Send sockets over SSL protocol. Default state is false.
*/
public $useSSL = false;
/**
* @var array PHP context options which are used in the Redis connection stream.
+ *
* @see https://www.php.net/manual/en/context.ssl.php
*/
public $contextOptions = [];
/**
- * @var integer Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](https://www.php.net/manual/en/function.stream-socket-client.php).
+ * @var int Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](https://www.php.net/manual/en/function.stream-socket-client.php).
* Currently the select of connection flags is limited to `STREAM_CLIENT_CONNECT` (default), `STREAM_CLIENT_ASYNC_CONNECT` and `STREAM_CLIENT_PERSISTENT`.
*
* > Warning: `STREAM_CLIENT_PERSISTENT` will make PHP reuse connections to the same server. If you are using multiple
@@ -326,19 +328,20 @@ class Connection extends Component
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT;
/**
- * @var integer The number of times a command execution should be retried when a connection failure occurs.
+ * @var int The number of times a command execution should be retried when a connection failure occurs.
* This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
* Defaults to 0 meaning no retries on failure.
*/
public $retries = 0;
/**
- * @var integer The retry interval in microseconds to wait between retry.
+ * @var int The retry interval in microseconds to wait between retry.
* This is used in [[executeCommand()]] when a [[SocketException]] is thrown.
* Defaults to 0 meaning no wait.
*/
public $retryInterval = 0;
/**
* @var array List of available redis commands.
+ *
* @see https://redis.io/commands
*/
public $redisCommands = [
@@ -560,9 +563,9 @@ class Connection extends Component
*/
private $_pool = [];
-
/**
* Closes the connection when this component is being serialized.
+ *
* @return array
*/
public function __sleep()
@@ -574,6 +577,7 @@ public function __sleep()
/**
* Return the connection string used to open a socket connection. During a redirect (cluster mode) this will be the
* target of the redirect.
+ *
* @return string socket connection string
*/
public function getConnectionString()
@@ -587,7 +591,8 @@ public function getConnectionString()
/**
* Return the connection resource if a connection to the target has been established before, `false` otherwise.
- * @return resource|false
+ *
+ * @return false|resource
*/
public function getSocket()
{
@@ -596,6 +601,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()
@@ -606,6 +612,7 @@ public function getIsActive()
/**
* Establishes a DB connection.
* It does nothing if a DB connection has already been established.
+ *
* @throws Exception if connection fails
*/
public function open()
@@ -680,6 +687,7 @@ protected function initConnection()
/**
* Returns the name of the DB driver for the current [[dsn]].
+ *
* @return string name of the DB driver
*/
public function getDriverName()
@@ -704,6 +712,7 @@ public function getLuaScriptBuilder()
*
* @param string $name name of the missing method to execute
* @param array $params method call arguments
+ *
* @return mixed
*/
public function __call($name, $params)
@@ -729,7 +738,10 @@ public function __call($name, $params)
*
* @param string $name the name of the command
* @param array $params list of parameters for the command
- * @return array|bool|null|string Dependent on the executed command this method
+ *
+ * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
+ *
+ * @return array|bool|string|null Dependent on the executed command this method
* will return different data types:
*
* - `true` for commands that return "status reply" with the message `'OK'` or `'PONG'`.
@@ -741,7 +753,6 @@ public function __call($name, $params)
*
* See [redis protocol description](https://redis.io/topics/protocol)
* for details on the mentioned reply types.
- * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
*/
public function executeCommand($name, $params = [])
{
@@ -793,7 +804,10 @@ public function executeCommand($name, $params = [])
* @param string $command command string
* @param array $params list of parameters for the command
*
- * @return array|bool|null|string Dependent on the executed command this method
+ * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
+ * @throws SocketException on connection error.
+ *
+ * @return array|bool|string|null Dependent on the executed command this method
* will return different data types:
*
* - `true` for commands that return "status reply" with the message `'OK'` or `'PONG'`.
@@ -805,8 +819,6 @@ public function executeCommand($name, $params = [])
*
* See [redis protocol description](https://redis.io/topics/protocol)
* for details on the mentioned reply types.
- * @throws Exception for commands that return [error reply](https://redis.io/topics/protocol#error-reply).
- * @throws SocketException on connection error.
*/
protected function sendRawCommand($command, $params)
{
@@ -824,9 +836,11 @@ protected function sendRawCommand($command, $params)
/**
* @param array $params
* @param string|null $command
- * @return mixed
+ *
* @throws Exception on error
* @throws SocketException
+ *
+ * @return mixed
*/
private function parseResponse($params, $command = null)
{
@@ -850,7 +864,7 @@ private function parseResponse($params, $command = null)
return $this->redirect($line, $command, $params);
}
- throw new Exception("Redis error: " . $line . "\nRedis command was: " . $prettyCommand);
+ throw new Exception('Redis error: ' . $line . "\nRedis command was: " . $prettyCommand);
case ':': // Integer reply
// no cast to int as it is in the range of a signed 64 bit integer
return $line;
@@ -884,6 +898,7 @@ private function parseResponse($params, $command = null)
/**
* @param string $line
+ *
* @return bool
*/
private function isRedirect($line)
@@ -895,9 +910,11 @@ private function isRedirect($line)
* @param string $redirect
* @param string $command
* @param array $params
- * @return mixed
+ *
* @throws Exception
* @throws SocketException
+ *
+ * @return mixed
*/
private function redirect($redirect, $command, $params)
{
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index f0bd9e8..19acd15 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -16,7 +16,9 @@ class LuaScriptBuilder extends \yii\base\BaseObject
{
/**
* Builds a Lua script for finding a list of records
+ *
* @param ActiveQuery $query the query used to build the script
+ *
* @return string
*/
public function buildAll($query)
@@ -30,7 +32,9 @@ public function buildAll($query)
/**
* Builds a Lua script for finding one record
+ *
* @param ActiveQuery $query the query used to build the script
+ *
* @return string
*/
public function buildOne($query)
@@ -44,8 +48,10 @@ public function buildOne($query)
/**
* Builds a Lua script for finding a column
+ *
* @param ActiveQuery $query the query used to build the script
* @param string $column name of the column
+ *
* @return string
*/
public function buildColumn($query, $column)
@@ -55,12 +61,14 @@ public function buildColumn($query, $column)
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
- return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks');
+ return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ')', 'pks');
}
/**
* Builds a Lua script for getting count of records
+ *
* @param ActiveQuery $query the query used to build the script
+ *
* @return string
*/
public function buildCount($query)
@@ -70,8 +78,10 @@ public function buildCount($query)
/**
* Builds a Lua script for finding the sum of a column
+ *
* @param ActiveQuery $query the query used to build the script
* @param string $column name of the column
+ *
* @return string
*/
public function buildSum($query, $column)
@@ -80,13 +90,15 @@ public function buildSum($query, $column)
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
- return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n');
+ return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ')', 'n');
}
/**
* Builds a Lua script for finding the average of a column
+ *
* @param ActiveQuery $query the query used to build the script
* @param string $column name of the column
+ *
* @return string
*/
public function buildAverage($query, $column)
@@ -95,13 +107,15 @@ public function buildAverage($query, $column)
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
- return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n');
+ return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ')', 'v/n');
}
/**
* Builds a Lua script for finding the min value of a column
+ *
* @param ActiveQuery $query the query used to build the script
* @param string $column name of the column
+ *
* @return string
*/
public function buildMin($query, $column)
@@ -110,13 +124,15 @@ public function buildMin($query, $column)
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
- return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nbuild($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ') if v==nil or nmodelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
- return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v');
+ return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ') if v==nil or n>v then v=n end', 'v');
}
/**
* @param ActiveQuery $query the query used to build the script
* @param string $buildResult the lua script for building the result
* @param string $return the lua variable that should be returned
+ *
* @throws NotSupportedException when query contains unsupported order by condition
+ *
* @return string
*/
private function build($query, $buildResult, $return)
@@ -205,8 +223,10 @@ private function build($query, $buildResult, $return)
/**
* Adds a column to the list of columns to retrieve and creates an alias
+ *
* @param string $column the column name to add
* @param array $columns list of columns given by reference
+ *
* @return string the alias generated for the column name
*/
private function addColumn($column, &$columns)
@@ -214,7 +234,7 @@ private function addColumn($column, &$columns)
if (isset($columns[$column])) {
return $columns[$column];
}
- $name = 'c' . preg_replace("/[^a-z]+/i", "", $column) . count($columns);
+ $name = 'c' . preg_replace('/[^a-z]+/i', '', $column) . count($columns);
return $columns[$column] = $name;
}
@@ -222,7 +242,9 @@ private function addColumn($column, &$columns)
/**
* Quotes a string value for use in a query.
* Note that if the parameter is not a string or int, it will be returned without change.
+ *
* @param string $str string to be quoted
+ *
* @return string the properly quoted string
*/
private function quoteValue($str)
@@ -236,12 +258,15 @@ private function quoteValue($str)
/**
* Parses the condition specification and generates the corresponding Lua expression.
- * @param string|array $condition the condition specification. Please refer to [[ActiveQuery::where()]]
+ *
+ * @param array|string $condition the condition specification. Please refer to [[ActiveQuery::where()]]
* on how to specify a condition.
* @param array $columns the list of columns and aliases to be used
- * @return string the generated SQL expression
+ *
* @throws \yii\db\Exception if the condition is in bad format
* @throws \yii\base\NotSupportedException if the condition is not an array
+ *
+ * @return string the generated SQL expression
*/
public function buildCondition($condition, &$columns)
{
@@ -293,7 +318,7 @@ private function buildHashCondition($condition, &$columns)
$value = (int) $value;
}
if ($value === null) {
- $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, " . $this->quoteValue($column) . ')==0';
} elseif ($value instanceof Expression) {
$column = $this->addColumn($column, $columns);
$parts[] = "$column==" . $value->expression;
@@ -346,7 +371,7 @@ private function buildBetweenCondition($operator, $operands, &$columns)
throw new Exception("Operator '$operator' requires three operands.");
}
- list($column, $value1, $value2) = $operands;
+ [$column, $value1, $value2] = $operands;
$value1 = $this->quoteValue($value1);
$value2 = $this->quoteValue($value2);
@@ -362,7 +387,7 @@ private function buildInCondition($operator, $operands, &$columns)
throw new Exception("Operator '$operator' requires two operands.");
}
- list($column, $values) = $operands;
+ [$column, $values] = $operands;
$values = (array) $values;
@@ -381,10 +406,10 @@ private function buildInCondition($operator, $operands, &$columns)
$parts = [];
foreach ($values as $value) {
if (is_array($value)) {
- $value = isset($value[$column]) ? $value[$column] : null;
+ $value = $value[$column] ?? null;
}
if ($value === null) {
- $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ $parts[] = "redis.call('HEXISTS',key .. ':a:' .. pk, " . $this->quoteValue($column) . ')==0';
} elseif ($value instanceof Expression) {
$parts[] = "$columnAlias==" . $value->expression;
} else {
@@ -407,7 +432,7 @@ protected function buildCompositeInCondition($operator, $inColumns, $values, &$c
$columnAlias = $this->addColumn($column, $columns);
$vs[] = "$columnAlias==" . $this->quoteValue($value[$column]);
} else {
- $vs[] = "redis.call('HEXISTS',key .. ':a:' .. pk, ".$this->quoteValue($column).")==0";
+ $vs[] = "redis.call('HEXISTS',key .. ':a:' .. pk, " . $this->quoteValue($column) . ')==0';
}
}
$vss[] = '(' . implode(' and ', $vs) . ')';
@@ -423,10 +448,10 @@ protected function buildCompareCondition($operator, $operands, &$columns)
throw new Exception("Operator '$operator' requires two operands.");
}
- list($column, $value) = $operands;
+ [$column, $value] = $operands;
$column = $this->addColumn($column, $columns);
- if (is_numeric($value)){
+ if (is_numeric($value)) {
return "tonumber($column) $operator $value";
}
$value = $this->quoteValue($value);
diff --git a/src/Mutex.php b/src/Mutex.php
index 9158dec..b2988ab 100644
--- a/src/Mutex.php
+++ b/src/Mutex.php
@@ -66,7 +66,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 array|Connection|string 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
@@ -79,10 +79,10 @@ class Mutex extends \yii\mutex\Mutex
*/
private $_lockValues = [];
-
/**
* Initializes the redis Mutex component.
* This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ *
* @throws InvalidConfigException if [[redis]] is invalid.
*/
public function init()
@@ -96,9 +96,11 @@ public function init()
/**
* Acquires a lock by name.
+ *
* @param string $name of the lock to be acquired. Must be unique.
* @param int $timeout time (in seconds) to wait for lock to be released. Defaults to `0` meaning that method will return
* false immediately in case lock was already acquired.
+ *
* @return bool lock acquiring result.
*/
protected function acquireLock($name, $timeout = 0)
@@ -118,7 +120,9 @@ protected function acquireLock($name, $timeout = 0)
/**
* Releases acquired lock. This method will return `false` in case the lock was not found or Redis command failed.
+ *
* @param string $name of the lock to be released. This lock must already exist.
+ *
* @return bool lock release result: `false` in case named lock was not found or Redis command failed.
*/
protected function releaseLock($name)
@@ -148,7 +152,9 @@ protected function releaseLock($name)
/**
* Generates a unique key used for storing the mutex in Redis.
+ *
* @param string $name mutex name.
+ *
* @return string a safe cache key associated with the mutex name.
*/
protected function calculateKey($name)
diff --git a/src/Session.php b/src/Session.php
index b318c04..df3fdc0 100644
--- a/src/Session.php
+++ b/src/Session.php
@@ -46,12 +46,12 @@
* ]
* ~~~
*
- * @property-read bool $useCustomStorage Whether to use custom storage.
+ * @property bool $useCustomStorage Whether to use custom storage.
*/
class Session extends \yii\web\Session
{
/**
- * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]].
+ * @var array|Connection|string 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
@@ -66,10 +66,10 @@ class Session extends \yii\web\Session
*/
public $keyPrefix;
-
/**
* Initializes the redis Session component.
* This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ *
* @throws InvalidConfigException if [[redis]] is invalid.
*/
public function init()
@@ -84,6 +84,7 @@ public function init()
/**
* Returns a value indicating whether to use custom session storage.
* This method overrides the parent implementation and always returns true.
+ *
* @return bool whether to use custom storage.
*/
public function getUseCustomStorage()
@@ -93,9 +94,12 @@ public function getUseCustomStorage()
/**
* Session open handler.
+ *
* @internal Do not call this method directly.
+ *
* @param string $savePath session save path
* @param string $sessionName session name
+ *
* @return bool whether session is opened successfully
*/
public function openSession($savePath, $sessionName)
@@ -114,7 +118,9 @@ public function openSession($savePath, $sessionName)
/**
* Session read handler.
* Do not call this method directly.
+ *
* @param string $id session ID
+ *
* @return string the session data
*/
public function readSession($id)
@@ -127,8 +133,10 @@ public function readSession($id)
/**
* Session write handler.
* Do not call this method directly.
+ *
* @param string $id session ID
* @param string $data session data
+ *
* @return bool whether session write is successful
*/
public function writeSession($id, $data)
@@ -144,7 +152,9 @@ public function writeSession($id, $data)
/**
* Session destroy handler.
* Do not call this method directly.
+ *
* @param string $id session ID
+ *
* @return bool whether session is destroyed successfully
*/
public function destroySession($id)
@@ -156,7 +166,9 @@ public function destroySession($id)
/**
* Generates a unique key used for storing session data in cache.
+ *
* @param string $id session variable name
+ *
* @return string a safe cache key associated with the session variable name
*/
protected function calculateKey($id)
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index 61e8395..ee1ed22 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -85,8 +85,8 @@ public function setUp(): void
$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');
+ // 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);
@@ -165,7 +165,6 @@ public function setUp(): void
$orderItem = new OrderItemWithNullFK();
$orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false);
$orderItem->save(false);
-
}
/**
@@ -456,7 +455,6 @@ public function testNotCondition()
$this->assertEquals(1, $orders[0]['customer_id']);
}
-
public function testBetweenCondition()
{
/* @var $orderClass \yii\db\ActiveRecordInterface */
@@ -523,10 +521,10 @@ public function testValueEscapingInWhere($filterWithInjection, $expectedStrings,
$lua = new LuaScriptBuilder();
$script = $lua->buildOne($query);
- foreach($expectedStrings as $string) {
+ foreach ($expectedStrings as $string) {
$this->assertStringContainsString($string, $script);
}
- foreach($unexpectedStrings as $string) {
+ foreach ($unexpectedStrings as $string) {
$this->assertStringNotContainsString($string, $script);
}
}
@@ -539,14 +537,14 @@ public function testValueEscapingInFindByCondition($filterWithInjection, $expect
/* @var $itemClass \yii\db\ActiveRecordInterface */
$itemClass = $this->getItemClass();
- $query = $this->invokeMethod(new $itemClass, 'findByCondition', [$filterWithInjection['id']]);
+ $query = $this->invokeMethod(new $itemClass(), 'findByCondition', [$filterWithInjection['id']]);
$lua = new LuaScriptBuilder();
$script = $lua->buildOne($query);
- foreach($expectedStrings as $string) {
+ foreach ($expectedStrings as $string) {
$this->assertStringContainsString($string, $script);
}
- foreach($unexpectedStrings as $string) {
+ foreach ($unexpectedStrings as $string) {
$this->assertStringNotContainsString($string, $script);
}
// ensure injected FLUSHALL call did not succeed
diff --git a/tests/CacheTestCase.php b/tests/CacheTestCase.php
index c3f5b85..e582c56 100644
--- a/tests/CacheTestCase.php
+++ b/tests/CacheTestCase.php
@@ -255,11 +255,15 @@ public function testGetOrSetWithDependencies()
$dependency = new TagDependency(['tags' => 'test']);
$expected = 'SilverFire';
- $loginClosure = function ($cache) use (&$login) { return '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'; };
+ $loginClosure = function ($cache) use (&$login) {
+ return 'SamDark';
+ };
$this->assertEquals($expected, $cache->getOrSet('some-login', $loginClosure, null, $dependency));
$dependency->invalidate($cache, 'test');
diff --git a/tests/RedisCacheTest.php b/tests/RedisCacheTest.php
index f26cb12..32109ca 100644
--- a/tests/RedisCacheTest.php
+++ b/tests/RedisCacheTest.php
@@ -9,6 +9,7 @@
/**
* Class for testing redis cache backend
+ *
* @group redis
* @group caching
*/
@@ -22,7 +23,7 @@ class RedisCacheTest extends CacheTestCase
protected function getCacheInstance()
{
$databases = TestCase::getParam('databases');
- $params = isset($databases['redis']) ? $databases['redis'] : null;
+ $params = $databases['redis'] ?? null;
if ($params === null) {
$this->markTestSkipped('No redis server connection configured.');
}
@@ -144,12 +145,12 @@ public function testReplica()
$this->assertSame($cache->get($key), $value);
$databases = TestCase::getParam('databases');
- $redis = isset($databases['redis']) ? $databases['redis'] : null;
+ $redis = $databases['redis'] ?? null;
$cache->replicas = [
[
- 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
- 'password' => isset($redis['password']) ? $redis['password'] : null,
+ 'hostname' => $redis['hostname'] ?? 'localhost',
+ 'password' => $redis['password'] ?? null,
],
];
$this->assertSame($cache->get($key), $value);
@@ -160,8 +161,8 @@ public function testReplica()
$cache->enableReplicas = true;
$cache->replicas = [
[
- 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
- 'password' => isset($redis['password']) ? $redis['password'] : null,
+ 'hostname' => $redis['hostname'] ?? 'localhost',
+ 'password' => $redis['password'] ?? null,
],
];
$this->assertFalse($cache->get($key));
@@ -175,12 +176,12 @@ public function testReplica()
$cache->replicas = [
[
- 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
- 'password' => isset($redis['password']) ? $redis['password'] : null,
+ 'hostname' => $redis['hostname'] ?? 'localhost',
+ 'password' => $redis['password'] ?? null,
],
[
- 'hostname' => isset($redis['hostname']) ? $redis['hostname'] : 'localhost',
- 'password' => isset($redis['password']) ? $redis['password'] : null,
+ 'hostname' => $redis['hostname'] ?? 'localhost',
+ 'password' => $redis['password'] ?? null,
],
];
$this->assertFalse($cache->get($key));
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index 585530d..9d3dd0d 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -64,6 +64,7 @@ public function testReConnect()
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::keyValueData
+ *
* @param mixed $data
*/
public function testStoreGet($data)
@@ -124,7 +125,9 @@ 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(function($s) {
+ return (string) $s;
+ }, ArrayHelper::getColumn($logger->messages, 0)), true));
}
public function testConnectionTimeoutRetryWithFirstFail()
@@ -133,7 +136,7 @@ public function testConnectionTimeoutRetryWithFirstFail()
Yii::setLogger($logger);
$databases = TestCase::getParam('databases');
- $redis = isset($databases['redis']) ? $databases['redis'] : [];
+ $redis = $databases['redis'] ?? [];
$db = new ConnectionWithErrorEmulator($redis);
$db->retries = 3;
@@ -150,7 +153,9 @@ 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(function($s) {
+ return (string) $s;
+ }, ArrayHelper::getColumn($logger->messages, 0)), true));
}
/**
@@ -181,7 +186,9 @@ public function testConnectionTimeoutRetryCount()
}
$this->assertTrue($exception, 'SocketException should have been thrown.');
$this->assertCount(14, $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(function($s) {
+ return (string) $s;
+ }, ArrayHelper::getColumn($logger->messages, 0)), true));
}
/**
@@ -213,13 +220,14 @@ public function testReturnType()
public function testTwoWordCommands()
{
$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'));
}
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::zRangeByScoreData
+ *
* @param array $members
* @param array $cases
*/
@@ -228,12 +236,12 @@ public function testZRangeByScore($members, $cases)
$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) {
@@ -243,7 +251,7 @@ public function testZRangeByScore($members, $cases)
} else {
$rows = $redis->zrangebyscore($set, $min, $max);
}
- $this->assertTrue(is_array($rows));
+ $this->assertIsArray($rows);
$this->assertEquals(count($expectedRows), count($rows));
for ($i = 0; $i < count($expectedRows); $i++) {
$this->assertEquals($expectedRows[$i], $rows[$i]);
@@ -253,6 +261,7 @@ public function testZRangeByScore($members, $cases)
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::hmSetData
+ *
* @param array $params
* @param array $pairs
*/
@@ -261,7 +270,7 @@ public function testHMSet($params, $pairs)
$redis = $this->getConnection();
$set = $params[0];
call_user_func_array([$redis,'hmset'], $params);
- foreach($pairs as $field => $expected) {
+ foreach ($pairs as $field => $expected) {
$actual = $redis->hget($set, $field);
$this->assertEquals($expected, $actual);
}
diff --git a/tests/RedisMutexTest.php b/tests/RedisMutexTest.php
index 31fcc94..f3ca64f 100644
--- a/tests/RedisMutexTest.php
+++ b/tests/RedisMutexTest.php
@@ -10,6 +10,7 @@
/**
* Class for testing redis mutex
+ *
* @group redis
* @group mutex
*/
@@ -94,7 +95,7 @@ protected function setUp(): void
{
parent::setUp();
$databases = TestCase::getParam('databases');
- $params = isset($databases['redis']) ? $databases['redis'] : null;
+ $params = $databases['redis'] ?? null;
if ($params === null) {
$this->markTestSkipped('No redis server connection configured.');
@@ -106,15 +107,16 @@ protected function setUp(): void
}
/**
- * @return Mutex
* @throws \yii\base\InvalidConfigException
+ *
+ * @return Mutex
*/
protected function createMutex()
{
return Yii::createObject([
'class' => Mutex::className(),
'expire' => 1.5,
- 'keyPrefix' => static::$mutexPrefix
+ 'keyPrefix' => static::$mutexPrefix,
]);
}
diff --git a/tests/RedisSessionTest.php b/tests/RedisSessionTest.php
index 08d7f97..d58be5d 100644
--- a/tests/RedisSessionTest.php
+++ b/tests/RedisSessionTest.php
@@ -5,10 +5,10 @@
namespace yiiunit\extensions\redis;
use yii\redis\Session;
-use yii\web\DbSession;
/**
* Class for testing redis session backend
+ *
* @group redis
* @group session
*/
@@ -26,6 +26,7 @@ public function testReadWrite()
/**
* Test set name. Also check set name twice and after open
+ *
* @runInSeparateProcess
*/
public function testSetName()
@@ -45,6 +46,7 @@ public function testSetName()
/**
* @depends testReadWrite
+ *
* @runInSeparateProcess
*/
public function testStrictMode()
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 07f7894..c79ae14 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -16,11 +16,12 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
{
public static $params;
-
/**
* Returns a test configuration param from /data/config.php
+ *
* @param string $name params name
* @param mixed $default default value to use when param is not set.
+ *
* @return mixed the value of the configuration param
*/
public static function getParam($name, $default = null)
@@ -29,7 +30,7 @@ public static function getParam($name, $default = null)
static::$params = require(__DIR__ . '/data/config.php');
}
- return isset(static::$params[$name]) ? static::$params[$name] : $default;
+ return static::$params[$name] ?? $default;
}
/**
@@ -45,6 +46,7 @@ protected function tearDown(): void
/**
* 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
*/
@@ -75,7 +77,7 @@ protected function mockWebApplication(array $config = [], $appClass = '\yii\web\
'scriptFile' => __DIR__ . '/index.php',
'scriptUrl' => '/index.php',
],
- ]
+ ],
], $config));
}
@@ -91,7 +93,7 @@ protected function destroyApplication()
protected function setUp(): void
{
$databases = self::getParam('databases');
- $params = isset($databases['redis']) ? $databases['redis'] : null;
+ $params = $databases['redis'] ?? null;
$this->assertNotNull($params, 'No redis server connection configured.');
$this->mockApplication(['components' => ['redis' => new Connection($params)]]);
@@ -100,13 +102,14 @@ protected function setUp(): void
}
/**
- * @param boolean $reset whether to clean up the test database
+ * @param bool $reset whether to clean up the test database
+ *
* @return Connection
*/
public function getConnection($reset = true)
{
$databases = self::getParam('databases');
- $params = isset($databases['redis']) ? $databases['redis'] : [];
+ $params = $databases['redis'] ?? [];
$db = new Connection($params);
if ($reset) {
$db->open();
@@ -123,10 +126,12 @@ public static function tearDownAfterClass(): void
/**
* 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)
diff --git a/tests/UniqueValidatorTest.php b/tests/UniqueValidatorTest.php
index 4377995..8ac4824 100644
--- a/tests/UniqueValidatorTest.php
+++ b/tests/UniqueValidatorTest.php
@@ -153,5 +153,4 @@ public function testValidationUpdateCompositePkUniqueAttribute()
$validator->validateAttribute($model1, 'quantity');
$this->assertTrue($model1->hasErrors('quantity'));
}
-
}
diff --git a/tests/data/ar/ActiveRecordTestTrait.php b/tests/data/ar/ActiveRecordTestTrait.php
index c6d6fa6..7dd15d1 100644
--- a/tests/data/ar/ActiveRecordTestTrait.php
+++ b/tests/data/ar/ActiveRecordTestTrait.php
@@ -18,6 +18,7 @@
* It is used directly in the unit tests for database active records in `tests/framework/db/ActiveRecordTest.php`
* but also used in the test suites of `redis`, `mongodb`, `elasticsearch` and `sphinx` AR implementations
* in the extensions.
+ *
* @see https://github.com/yiisoft/yii2-redis/blob/a920547708c4a7091896923abc2499bc8c1c0a3b/tests/bootstrap.php#L17-L26
*/
trait ActiveRecordTestTrait
@@ -25,24 +26,28 @@ trait ActiveRecordTestTrait
/* @var $this TestCase */
/**
* This method should return the classname of Customer class.
+ *
* @return string
*/
abstract public function getCustomerClass();
/**
* This method should return the classname of Order class.
+ *
* @return string
*/
abstract public function getOrderClass();
/**
* This method should return the classname of OrderItem class.
+ *
* @return string
*/
abstract public function getOrderItemClass();
/**
* This method should return the classname of Item class.
+ *
* @return string
*/
abstract public function getItemClass();
diff --git a/tests/data/ar/Customer.php b/tests/data/ar/Customer.php
index d0f8c1a..78a1aec 100644
--- a/tests/data/ar/Customer.php
+++ b/tests/data/ar/Customer.php
@@ -25,8 +25,8 @@
*/
class Customer extends ActiveRecord
{
- const STATUS_ACTIVE = 1;
- const STATUS_INACTIVE = 2;
+ public const STATUS_ACTIVE = 1;
+ public const STATUS_INACTIVE = 2;
public $status2;
@@ -98,10 +98,11 @@ public function afterSave($insert, $changedAttributes)
/**
* @inheritdoc
+ *
* @return CustomerQuery
*/
public static function find()
{
- return new CustomerQuery(get_called_class());
+ return new CustomerQuery(static::class);
}
}
diff --git a/tests/data/ar/Order.php b/tests/data/ar/Order.php
index cf7c5cb..f30f9f6 100644
--- a/tests/data/ar/Order.php
+++ b/tests/data/ar/Order.php
@@ -11,7 +11,6 @@
* @property int $customer_id
* @property int $created_at
* @property string $total
- *
* @property Customer $customer
* @property Item[] $itemsIndexed
* @property OrderItem[] $orderItems
@@ -22,9 +21,8 @@
* @property Item[] $itemsWithNullFK
* @property OrderItemWithNullFK[] $orderItemsWithNullFK
* @property Item[] $books
- *
- * @property-read Item[] $expensiveItemsUsingViaWithCallable
- * @property-read Item[] $cheapItemsUsingViaWithCallable
+ * @property Item[] $expensiveItemsUsingViaWithCallable
+ * @property Item[] $cheapItemsUsingViaWithCallable
*/
class Order extends ActiveRecord
{
@@ -68,15 +66,15 @@ public function getExpensiveItemsUsingViaWithCallable()
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems', function (\yii\redis\ActiveQuery $q) {
$q->where(['>=', 'subtotal', 10]);
- });
- }
+ });
+ }
public function getCheapItemsUsingViaWithCallable()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems', function (\yii\redis\ActiveQuery $q) {
- $q->where(['<', 'subtotal', 10]);
- });
+ $q->where(['<', 'subtotal', 10]);
+ });
}
/**
@@ -156,8 +154,7 @@ public function beforeSave($insert)
$this->created_at = time();
return true;
- } else {
- return false;
}
+ return false;
}
}
diff --git a/tests/data/ar/OrderItem.php b/tests/data/ar/OrderItem.php
index 56b1f3c..435d4e3 100644
--- a/tests/data/ar/OrderItem.php
+++ b/tests/data/ar/OrderItem.php
@@ -11,7 +11,6 @@
* @property int $item_id
* @property int $quantity
* @property string $subtotal
- *
* @property Order $order
* @property Item $item
*/
diff --git a/tests/data/ar/OrderItemWithNullFK.php b/tests/data/ar/OrderItemWithNullFK.php
index b51f626..58441b0 100644
--- a/tests/data/ar/OrderItemWithNullFK.php
+++ b/tests/data/ar/OrderItemWithNullFK.php
@@ -7,9 +7,9 @@
/**
* Class OrderItem
*
- * @property integer $order_id
- * @property integer $item_id
- * @property integer $quantity
+ * @property int $order_id
+ * @property int $item_id
+ * @property int $quantity
* @property string $subtotal
*/
class OrderItemWithNullFK extends ActiveRecord
diff --git a/tests/data/ar/OrderWithNullFK.php b/tests/data/ar/OrderWithNullFK.php
index a14726d..9e42c2a 100644
--- a/tests/data/ar/OrderWithNullFK.php
+++ b/tests/data/ar/OrderWithNullFK.php
@@ -7,9 +7,9 @@
/**
* Class Order
*
- * @property integer $id
- * @property integer $customer_id
- * @property integer $created_at
+ * @property int $id
+ * @property int $customer_id
+ * @property int $created_at
* @property string $total
*/
class OrderWithNullFK extends ActiveRecord
diff --git a/tests/providers/Data.php b/tests/providers/Data.php
index 2c98ffc..336352d 100644
--- a/tests/providers/Data.php
+++ b/tests/providers/Data.php
@@ -24,7 +24,7 @@ public static function hmSetData(): array
[
'one' => '1',
'two' => '2',
- 'three' => '3'
+ 'three' => '3',
],
],
[
@@ -32,9 +32,9 @@ public static function hmSetData(): array
[
'one' => '',
'two' => '2',
- 'three' => '3'
+ 'three' => '3',
],
- ]
+ ],
];
}
@@ -50,8 +50,8 @@ public static function illegalValuesForFindByCondition(): array
]], ["'`id`=`id` and 1'", 'ididand']],
[['id' => [
'nested_illegal' => [
- 'false or 1=' => 1
- ]
+ 'false or 1=' => 1,
+ ],
]], [], ['false or 1=']],
// custom condition injection
@@ -68,7 +68,7 @@ public static function illegalValuesForFindByCondition(): array
[['id' => [
'name' => 'test',
'email' => 'test@example.com',
- "' .. redis.call('FLUSHALL') .. '" => "' .. redis.call('FLUSHALL') .. '"
+ "' .. redis.call('FLUSHALL') .. '" => "' .. redis.call('FLUSHALL') .. '",
]], ["'\\' .. redis.call(\\'FLUSHALL\\') .. \\'", 'rediscallFLUSHALL'], ["' .. redis.call('FLUSHALL') .. '"]],
];
}
@@ -84,8 +84,8 @@ public static function illegalValuesForWhere(): array
]], ["'`id`=`id` and 1'", 'ididand']],
[['id' => [
'nested_illegal' => [
- 'false or 1=' => 1
- ]
+ 'false or 1=' => 1,
+ ],
]], [], ['false or 1=']],
];
}
diff --git a/tests/support/ConnectionWithErrorEmulator.php b/tests/support/ConnectionWithErrorEmulator.php
index 6ab999b..411ce1e 100644
--- a/tests/support/ConnectionWithErrorEmulator.php
+++ b/tests/support/ConnectionWithErrorEmulator.php
@@ -1,10 +1,14 @@
Date: Tue, 17 Oct 2023 17:29:48 -0300
Subject: [PATCH 15/25] More fixes.
---
phpstan.neon | 2 +-
src/LuaScriptBuilder.php | 18 +++++++++---------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/phpstan.neon b/phpstan.neon
index 0b239cb..bfa8725 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -8,7 +8,7 @@ parameters:
- YII_ENV_PROD
- YII_ENV_TEST
- level: 2
+ level: 1
paths:
- src
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index 19acd15..8f74676 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -23,7 +23,7 @@ class LuaScriptBuilder extends \yii\base\BaseObject
*/
public function buildAll($query)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -39,7 +39,7 @@ public function buildAll($query)
*/
public function buildOne($query)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -56,8 +56,7 @@ public function buildOne($query)
*/
public function buildColumn($query, $column)
{
- // TODO add support for indexBy
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -86,7 +85,7 @@ public function buildCount($query)
*/
public function buildSum($query, $column)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -103,7 +102,7 @@ public function buildSum($query, $column)
*/
public function buildAverage($query, $column)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -120,7 +119,7 @@ public function buildAverage($query, $column)
*/
public function buildMin($query, $column)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -137,7 +136,7 @@ public function buildMin($query, $column)
*/
public function buildMax($query, $column)
{
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix() . ':a:');
@@ -156,6 +155,7 @@ public function buildMax($query, $column)
private function build($query, $buildResult, $return)
{
$columns = [];
+
if ($query->where !== null) {
$condition = $this->buildCondition($query->where, $columns);
} else {
@@ -165,7 +165,7 @@ private function build($query, $buildResult, $return)
$start = ($query->offset === null || $query->offset < 0) ? 0 : $query->offset;
$limitCondition = 'i>' . $start . (($query->limit === null || $query->limit < 0) ? '' : ' and i<=' . ($start + $query->limit));
- /* @var $modelClass ActiveRecord */
+ /** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$key = $this->quoteValue($modelClass::keyPrefix());
$loadColumnValues = '';
From 462cc69c0321881f4d9ec61acae952f3372a5dd1 Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Tue, 17 Oct 2023 20:30:05 +0000
Subject: [PATCH 16/25] Apply fixes from StyleCI
---
src/LuaScriptBuilder.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index 8f74676..dc263f7 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -155,7 +155,7 @@ public function buildMax($query, $column)
private function build($query, $buildResult, $return)
{
$columns = [];
-
+
if ($query->where !== null) {
$condition = $this->buildCondition($query->where, $columns);
} else {
From 260291ed061cb891fdec9e15d9ea18f2f22b66b9 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:34:29 -0300
Subject: [PATCH 17/25] More fixes.
---
src/Connection.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Connection.php b/src/Connection.php
index 7e0fbd8..44039d4 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -627,7 +627,7 @@ public function open()
$this->connectionString,
$errorNumber,
$errorDescription,
- $this->connectionTimeout ?: ini_get('default_socket_timeout'),
+ (float) $this->connectionTimeout ?: ini_get('default_socket_timeout'),
$this->socketClientFlags,
stream_context_create($this->contextOptions)
);
From bba7e7e7598c341db8e21614269b2812797b12f5 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:36:39 -0300
Subject: [PATCH 18/25] More fixes.
---
src/Connection.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Connection.php b/src/Connection.php
index 44039d4..3195ae9 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -627,7 +627,7 @@ public function open()
$this->connectionString,
$errorNumber,
$errorDescription,
- (float) $this->connectionTimeout ?: ini_get('default_socket_timeout'),
+ $this->connectionTimeout ?: ini_get('default_socket_timeout'),
$this->socketClientFlags,
stream_context_create($this->contextOptions)
);
@@ -762,7 +762,7 @@ public function executeCommand($name, $params = [])
$command = '*' . count($params) . "\r\n";
foreach ($params as $arg) {
- $command .= '$' . mb_strlen((string) $arg, '8bit') . "\r\n" . (string) $arg . "\r\n";
+ $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
}
\Yii::debug("Executing Redis Command: {$name}", __METHOD__);
From ceb3bf8d6664f34fefa52fc187cfefb9e5b0a23e Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:38:41 -0300
Subject: [PATCH 19/25] More fix.
---
src/Connection.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Connection.php b/src/Connection.php
index 3195ae9..6a8be3b 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -627,7 +627,7 @@ public function open()
$this->connectionString,
$errorNumber,
$errorDescription,
- $this->connectionTimeout ?: ini_get('default_socket_timeout'),
+ $this->connectionTimeout ?: (float) ini_get('default_socket_timeout'),
$this->socketClientFlags,
stream_context_create($this->contextOptions)
);
From aa385d648e65bb7b37d775ad408227f84fc59c13 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:40:13 -0300
Subject: [PATCH 20/25] More fix.
---
src/Connection.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Connection.php b/src/Connection.php
index 6a8be3b..aef33b0 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -762,7 +762,7 @@ public function executeCommand($name, $params = [])
$command = '*' . count($params) . "\r\n";
foreach ($params as $arg) {
- $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
+ $command .= '$' . mb_strlen((string) $arg, '8bit') . "\r\n" . (string) $arg . "\r\n";
}
\Yii::debug("Executing Redis Command: {$name}", __METHOD__);
From 699a09c641140b7582d621d57739c4872ada8e0c Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:42:08 -0300
Subject: [PATCH 21/25] More fix.
---
src/LuaScriptBuilder.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/LuaScriptBuilder.php b/src/LuaScriptBuilder.php
index dc263f7..5d6a830 100644
--- a/src/LuaScriptBuilder.php
+++ b/src/LuaScriptBuilder.php
@@ -253,7 +253,7 @@ private function quoteValue($str)
return $str;
}
- return "'" . addcslashes($str, "\000\n\r\\\032\047") . "'";
+ return "'" . addcslashes((string) $str, "\000\n\r\\\032\047") . "'";
}
/**
From 428b5f019ef00478d801e009e1eed0d024148b22 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:50:06 -0300
Subject: [PATCH 22/25] More fix.
---
tests/ActiveDataProviderTest.php | 2 +-
tests/ActiveRecordTest.php | 52 ++++++-------
tests/CacheTestCase.php | 50 ++++++-------
tests/RedisCacheTest.php | 16 ++--
tests/RedisConnectionTest.php | 43 +++++------
tests/RedisMutexTest.php | 8 +-
tests/RedisSessionTest.php | 6 +-
tests/TestCase.php | 3 +-
tests/UniqueValidatorTest.php | 12 +--
tests/data/ar/ActiveRecordTestTrait.php | 98 ++++++++++++-------------
tests/data/ar/Customer.php | 6 +-
tests/data/ar/Order.php | 10 +--
12 files changed, 145 insertions(+), 161 deletions(-)
diff --git a/tests/ActiveDataProviderTest.php b/tests/ActiveDataProviderTest.php
index 05d6962..f3e25b4 100644
--- a/tests/ActiveDataProviderTest.php
+++ b/tests/ActiveDataProviderTest.php
@@ -26,7 +26,7 @@ protected function setUp(): void
$item->save(false);
}
- public function testQuery()
+ public function testQuery(): void
{
$query = Item::find();
$provider = new ActiveDataProvider(['query' => $query]);
diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php
index ee1ed22..2ac840c 100644
--- a/tests/ActiveRecordTest.php
+++ b/tests/ActiveRecordTest.php
@@ -105,13 +105,13 @@ public function setUp(): void
$item->save(false);
$order = new Order();
- $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false);
+ $order->setAttributes(['customer_id' => 1, 'created_at' => 1_325_282_384, 'total' => 110.0], false);
$order->save(false);
$order = new Order();
- $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false);
+ $order->setAttributes(['customer_id' => 2, 'created_at' => 1_325_334_482, 'total' => 33.0], false);
$order->save(false);
$order = new Order();
- $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false);
+ $order->setAttributes(['customer_id' => 2, 'created_at' => 1_325_502_201, 'total' => 40.0], false);
$order->save(false);
$orderItem = new OrderItem();
@@ -138,13 +138,13 @@ public function setUp(): void
$orderItem->save(false);
$order = new OrderWithNullFK();
- $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false);
+ $order->setAttributes(['customer_id' => 1, 'created_at' => 1_325_282_384, 'total' => 110.0], false);
$order->save(false);
$order = new OrderWithNullFK();
- $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false);
+ $order->setAttributes(['customer_id' => 2, 'created_at' => 1_325_334_482, 'total' => 33.0], false);
$order->save(false);
$order = new OrderWithNullFK();
- $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false);
+ $order->setAttributes(['customer_id' => 2, 'created_at' => 1_325_502_201, 'total' => 40.0], false);
$order->save(false);
$orderItem = new OrderItemWithNullFK();
@@ -170,7 +170,7 @@ public function setUp(): void
/**
* overridden because null values are not part of the asArray result in redis
*/
- public function testFindAsArray()
+ public function testFindAsArray(): void
{
/* @var $customerClass \yii\db\ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -205,7 +205,7 @@ public function testFindAsArray()
$this->assertArrayHasKey('status', $customers[2]);
}
- public function testStatisticalFind()
+ public function testStatisticalFind(): void
{
// find count, sum, average, min, max, scalar
$this->assertEquals(3, Customer::find()->count());
@@ -220,7 +220,7 @@ public function testStatisticalFind()
// TODO test serial column incr
- public function testUpdatePk()
+ public function testUpdatePk(): void
{
// updateCounters
$pk = ['order_id' => 2, 'item_id' => 4];
@@ -237,7 +237,7 @@ public function testUpdatePk()
$this->assertNotNull(OrderItem::findOne(['order_id' => 2, 'item_id' => 10]));
}
- public function testFilterWhere()
+ public function testFilterWhere(): void
{
// should work with hash format
$query = new ActiveQuery('dummy');
@@ -300,14 +300,14 @@ public function testFilterWhere()
$this->assertEquals($condition, $query->where);
}
- public function testFilterWhereRecursively()
+ 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()
+ public function testAutoIncrement(): void
{
Customer::getDb()->executeCommand('FLUSHDB');
@@ -348,7 +348,7 @@ public function testAutoIncrement()
$this->assertEquals('user6', $customer->name);
}
- public function testEscapeData()
+ public function testEscapeData(): void
{
$customer = new Customer();
$customer->email = "the People's Republic of China";
@@ -359,7 +359,7 @@ public function testEscapeData()
$this->assertSame("the People's Republic of China", $c->email);
}
- public function testFindEmptyWith()
+ public function testFindEmptyWith(): void
{
Order::getDb()->flushdb();
$orders = Order::find()
@@ -370,7 +370,7 @@ public function testFindEmptyWith()
$this->assertEquals([], $orders);
}
- public function testEmulateExecution()
+ public function testEmulateExecution(): void
{
$rows = Order::find()
->emulateExecution()
@@ -426,13 +426,13 @@ public function testEmulateExecution()
/**
* @see https://github.com/yiisoft/yii2-redis/issues/93
*/
- public function testDeleteAllWithCondition()
+ public function testDeleteAllWithCondition(): void
{
$deletedCount = Order::deleteAll(['in', 'id', [1, 2, 3]]);
$this->assertEquals(3, $deletedCount);
}
- public function testBuildKey()
+ public function testBuildKey(): void
{
$pk = ['order_id' => 3, 'item_id' => 'nostr'];
$key = OrderItem::buildKey($pk);
@@ -444,7 +444,7 @@ public function testBuildKey()
$this->assertEquals($key, OrderItem::buildKey($pk));
}
- public function testNotCondition()
+ public function testNotCondition(): void
{
/* @var $orderClass \yii\db\ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -455,7 +455,7 @@ public function testNotCondition()
$this->assertEquals(1, $orders[0]['customer_id']);
}
- public function testBetweenCondition()
+ public function testBetweenCondition(): void
{
/* @var $orderClass \yii\db\ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -471,7 +471,7 @@ public function testBetweenCondition()
$this->assertEquals(1, $orders[0]['customer_id']);
}
- public function testInCondition()
+ public function testInCondition(): void
{
/* @var $orderClass \yii\db\ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -493,7 +493,7 @@ public function testInCondition()
$this->assertEquals(2, $orders[1]['customer_id']);
}
- public function testCountQuery()
+ public function testCountQuery(): void
{
/* @var $itemClass \yii\db\ActiveRecordInterface */
$itemClass = $this->getItemClass();
@@ -512,7 +512,7 @@ public function testCountQuery()
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::illegalValuesForWhere
*/
- public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $unexpectedStrings = [])
+ public function testValueEscapingInWhere($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void
{
/* @var $itemClass \yii\db\ActiveRecordInterface */
$itemClass = $this->getItemClass();
@@ -532,7 +532,7 @@ public function testValueEscapingInWhere($filterWithInjection, $expectedStrings,
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::illegalValuesForFindByCondition
*/
- public function testValueEscapingInFindByCondition($filterWithInjection, $expectedStrings, $unexpectedStrings = [])
+ public function testValueEscapingInFindByCondition($filterWithInjection, $expectedStrings, $unexpectedStrings = []): void
{
/* @var $itemClass \yii\db\ActiveRecordInterface */
$itemClass = $this->getItemClass();
@@ -552,7 +552,7 @@ public function testValueEscapingInFindByCondition($filterWithInjection, $expect
$this->assertGreaterThan(3, $itemClass::find()->count());
}
- public function testCompareCondition()
+ public function testCompareCondition(): void
{
/* @var $orderClass \yii\db\ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -580,7 +580,7 @@ public function testCompareCondition()
$this->assertEquals(2, $orders[1]['customer_id']);
}
- public function testStringCompareCondition()
+ public function testStringCompareCondition(): void
{
/* @var $itemClass \yii\db\ActiveRecordInterface */
$itemClass = $this->getItemClass();
@@ -603,7 +603,7 @@ public function testStringCompareCondition()
$this->assertCount(2, $items);
}
- public function testFind()
+ public function testFind(): void
{
/* @var $customerClass \yii\db\ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
diff --git a/tests/CacheTestCase.php b/tests/CacheTestCase.php
index e582c56..7deb0af 100644
--- a/tests/CacheTestCase.php
+++ b/tests/CacheTestCase.php
@@ -56,7 +56,7 @@ public function prepare()
return $cache;
}
- public function testSet()
+ public function testSet(): void
{
$cache = $this->getCacheInstance();
@@ -65,7 +65,7 @@ public function testSet()
$this->assertTrue($cache->set('array_test', ['array_test' => 'array_test']));
}
- public function testGet()
+ public function testGet(): void
{
$cache = $this->prepare();
@@ -103,7 +103,7 @@ public function testMultiset(int $expiry): void
$this->assertEquals('array_test', $array['array_test']);
}
- public function testExists()
+ public function testExists(): void
{
$cache = $this->prepare();
@@ -115,7 +115,7 @@ public function testExists()
$this->assertFalse($cache->exists('not_exists'));
}
- public function testArrayAccess()
+ public function testArrayAccess(): void
{
$cache = $this->getCacheInstance();
@@ -123,21 +123,21 @@ public function testArrayAccess()
$this->assertInstanceOf('stdClass', $cache['arrayaccess_test']);
}
- public function testGetValueNonExistent()
+ public function testGetValueNonExistent(): void
{
$cache = $this->getCacheInstance();
$this->assertFalse($this->invokeMethod($cache, 'getValue', ['non_existent_key']));
}
- public function testGetNonExistent()
+ public function testGetNonExistent(): void
{
$cache = $this->getCacheInstance();
$this->assertFalse($cache->get('non_existent_key'));
}
- public function testStoreSpecialValues()
+ public function testStoreSpecialValues(): void
{
$cache = $this->getCacheInstance();
@@ -148,7 +148,7 @@ public function testStoreSpecialValues()
$this->assertTrue($cache->get('bool_value'));
}
- public function testMultiGet()
+ public function testMultiGet(): void
{
$cache = $this->prepare();
@@ -158,36 +158,36 @@ public function testMultiGet()
$this->assertSame(['number_test' => 42, 'non_existent_key' => false], $cache->multiGet(['number_test', 'non_existent_key']));
}
- public function testDefaultTtl()
+ public function testDefaultTtl(): void
{
$cache = $this->getCacheInstance();
$this->assertSame(0, $cache->defaultDuration);
}
- public function testExpire()
+ public function testExpire(): void
{
$cache = $this->getCacheInstance();
$this->assertTrue($cache->set('expire_test', 'expire_test', 2));
usleep(500000);
$this->assertEquals('expire_test', $cache->get('expire_test'));
- usleep(2500000);
+ usleep(2_500_000);
$this->assertFalse($cache->get('expire_test'));
}
- public function testExpireAdd()
+ public function testExpireAdd(): void
{
$cache = $this->getCacheInstance();
$this->assertTrue($cache->add('expire_testa', 'expire_testa', 2));
usleep(500000);
$this->assertEquals('expire_testa', $cache->get('expire_testa'));
- usleep(2500000);
+ usleep(2_500_000);
$this->assertFalse($cache->get('expire_testa'));
}
- public function testAdd()
+ public function testAdd(): void
{
$cache = $this->prepare();
@@ -201,7 +201,7 @@ public function testAdd()
$this->assertEquals(13, $cache->get('add_test'));
}
- public function testMultiAdd()
+ public function testMultiAdd(): void
{
$cache = $this->prepare();
@@ -216,7 +216,7 @@ public function testMultiAdd()
$this->assertEquals(13, $cache->get('add_test'));
}
- public function testDelete()
+ public function testDelete(): void
{
$cache = $this->prepare();
@@ -225,19 +225,19 @@ public function testDelete()
$this->assertFalse($cache->get('number_test'));
}
- public function testFlush()
+ public function testFlush(): void
{
$cache = $this->prepare();
$this->assertTrue($cache->flush());
$this->assertFalse($cache->get('number_test'));
}
- public function testGetOrSet()
+ public function testGetOrSet(): void
{
$cache = $this->prepare();
$expected = $this->getOrSetCallable($cache);
- $callable = [$this, 'getOrSetCallable'];
+ $callable = $this->getOrSetCallable(...);
$this->assertFalse($cache->get('something'));
$this->assertEquals($expected, $cache->getOrSet('something', $callable));
@@ -246,24 +246,20 @@ public function testGetOrSet()
public function getOrSetCallable($cache)
{
- return get_class($cache);
+ return $cache::class;
}
- public function testGetOrSetWithDependencies()
+ public function testGetOrSetWithDependencies(): void
{
$cache = $this->prepare();
$dependency = new TagDependency(['tags' => 'test']);
$expected = 'SilverFire';
- $loginClosure = function ($cache) use (&$login) {
- return 'SilverFire';
- };
+ $loginClosure = fn($cache) => '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';
- };
+ $loginClosure = fn($cache) => 'SamDark';
$this->assertEquals($expected, $cache->getOrSet('some-login', $loginClosure, null, $dependency));
$dependency->invalidate($cache, 'test');
diff --git a/tests/RedisCacheTest.php b/tests/RedisCacheTest.php
index 32109ca..0503f6e 100644
--- a/tests/RedisCacheTest.php
+++ b/tests/RedisCacheTest.php
@@ -15,7 +15,7 @@
*/
class RedisCacheTest extends CacheTestCase
{
- private $_cacheInstance;
+ private Cache|null $_cacheInstance = null;
/**
* @return Cache
@@ -47,7 +47,7 @@ protected function resetCacheInstance()
$this->_cacheInstance = null;
}
- public function testExpireMilliseconds()
+ public function testExpireMilliseconds(): void
{
$cache = $this->getCacheInstance();
@@ -58,7 +58,7 @@ public function testExpireMilliseconds()
$this->assertFalse($cache->get('expire_test_ms'));
}
- public function testExpireAddMilliseconds()
+ public function testExpireAddMilliseconds(): void
{
$cache = $this->getCacheInstance();
@@ -73,7 +73,7 @@ public function testExpireAddMilliseconds()
* Store a value that is 2 times buffer size big
* https://github.com/yiisoft/yii2/issues/743
*/
- public function testLargeData()
+ public function testLargeData(): void
{
$cache = $this->getCacheInstance();
@@ -97,7 +97,7 @@ public function testLargeData()
* Store a megabyte and see how it goes
* https://github.com/yiisoft/yii2/issues/6547
*/
- public function testReallyLargeData()
+ public function testReallyLargeData(): void
{
$cache = $this->getCacheInstance();
@@ -117,7 +117,7 @@ public function testReallyLargeData()
}
}
- public function testMultiByteGetAndSet()
+ public function testMultiByteGetAndSet(): void
{
$cache = $this->getCacheInstance();
@@ -129,7 +129,7 @@ public function testMultiByteGetAndSet()
$this->assertSame($cache->get($key), $data);
}
- public function testReplica()
+ public function testReplica(): void
{
$this->resetCacheInstance();
@@ -201,7 +201,7 @@ public function testReplica()
$this->resetCacheInstance();
}
- public function testFlushWithSharedDatabase()
+ public function testFlushWithSharedDatabase(): void
{
$instance = $this->getCacheInstance();
$instance->shareDatabase = true;
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index 9d3dd0d..b4bad8d 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -25,7 +25,7 @@ protected function tearDown(): void
/**
* test connection to redis and selection of db
*/
- public function testConnect()
+ public function testConnect(): void
{
$db = $this->getConnection(false);
$database = $db->database;
@@ -50,7 +50,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();
@@ -65,9 +65,8 @@ public function testReConnect()
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::keyValueData
*
- * @param mixed $data
*/
- public function testStoreGet($data)
+ public function testStoreGet(mixed $data): void
{
$db = $this->getConnection(true);
@@ -75,7 +74,7 @@ public function testStoreGet($data)
$this->assertEquals($data, $db->get('hi'));
}
- public function testSerialize()
+ public function testSerialize(): void
{
$db = $this->getConnection(false);
$db->open();
@@ -87,7 +86,7 @@ public function testSerialize()
$this->assertTrue($db2->ping());
}
- public function testConnectionTimeout()
+ public function testConnectionTimeout(): void
{
$db = $this->getConnection(false);
$db->configSet('timeout', 1);
@@ -103,7 +102,7 @@ public function testConnectionTimeout()
$this->assertTrue($db->ping());
}
- public function testConnectionTimeoutRetry()
+ public function testConnectionTimeoutRetry(): void
{
$logger = new Logger();
Yii::setLogger($logger);
@@ -125,12 +124,10 @@ 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(fn($s) => (string) $s, ArrayHelper::getColumn($logger->messages, 0)), true));
}
- public function testConnectionTimeoutRetryWithFirstFail()
+ public function testConnectionTimeoutRetryWithFirstFail(): void
{
$logger = new Logger();
Yii::setLogger($logger);
@@ -153,15 +150,13 @@ 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(fn($s) => (string) $s, ArrayHelper::getColumn($logger->messages, 0)), true));
}
/**
* Retry connecting 2 times
*/
- public function testConnectionTimeoutRetryCount()
+ public function testConnectionTimeoutRetryCount(): void
{
$logger = new Logger();
Yii::setLogger($logger);
@@ -169,7 +164,7 @@ public function testConnectionTimeoutRetryCount()
$db = $this->getConnection(false);
$db->retries = 2;
$db->configSet('timeout', 1);
- $db->on(Connection::EVENT_AFTER_OPEN, function() {
+ $db->on(Connection::EVENT_AFTER_OPEN, function(): void {
// sleep 2 seconds after connect to make every command time out
sleep(2);
});
@@ -181,20 +176,18 @@ public function testConnectionTimeoutRetryCount()
// results in 3 times sending the PING command to redis
sleep(2);
$db->ping();
- } catch (SocketException $e) {
+ } catch (SocketException) {
$exception = true;
}
$this->assertTrue($exception, 'SocketException should have been thrown.');
$this->assertCount(14, $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(fn($s) => (string) $s, ArrayHelper::getColumn($logger->messages, 0)), true));
}
/**
* https://github.com/yiisoft/yii2/issues/4745
*/
- public function testReturnType()
+ public function testReturnType(): void
{
$redis = $this->getConnection();
$redis->executeCommand('SET', ['key1', 'val1']);
@@ -217,7 +210,7 @@ public function testReturnType()
}
}
- public function testTwoWordCommands()
+ public function testTwoWordCommands(): void
{
$redis = $this->getConnection();
$this->assertIsArray($redis->executeCommand('CONFIG GET', ['port']));
@@ -231,7 +224,7 @@ public function testTwoWordCommands()
* @param array $members
* @param array $cases
*/
- public function testZRangeByScore($members, $cases)
+ public function testZRangeByScore($members, $cases): void
{
$redis = $this->getConnection();
$set = 'zrangebyscore';
@@ -265,11 +258,11 @@ public function testZRangeByScore($members, $cases)
* @param array $params
* @param array $pairs
*/
- public function testHMSet($params, $pairs)
+ public function testHMSet($params, $pairs): void
{
$redis = $this->getConnection();
$set = $params[0];
- call_user_func_array([$redis,'hmset'], $params);
+ 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/RedisMutexTest.php b/tests/RedisMutexTest.php
index f3ca64f..ca12120 100644
--- a/tests/RedisMutexTest.php
+++ b/tests/RedisMutexTest.php
@@ -20,9 +20,9 @@ class RedisMutexTest extends TestCase
protected static $mutexPrefix = 'prefix';
- private static $_keys = [];
+ private static array $_keys = [];
- public function testAcquireAndRelease()
+ public function testAcquireAndRelease(): void
{
$mutex = $this->createMutex();
@@ -39,7 +39,7 @@ public function testAcquireAndRelease()
$this->assertMutexKeyNotInRedis();
}
- public function testExpiration()
+ public function testExpiration(): void
{
$mutex = $this->createMutex();
@@ -60,7 +60,7 @@ public function testExpiration()
*
* @dataProvider \yiiunit\extensions\redis\providers\Data::acquireTimeout
*/
- public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTimeout, $lockIsReleased)
+ public function testConcurentMutexAcquireAndRelease($timeout, $canAcquireAfterTimeout, $lockIsReleased): void
{
$mutexOne = $this->createMutex();
$mutexTwo = $this->createMutex();
diff --git a/tests/RedisSessionTest.php b/tests/RedisSessionTest.php
index d58be5d..ac88e3d 100644
--- a/tests/RedisSessionTest.php
+++ b/tests/RedisSessionTest.php
@@ -14,7 +14,7 @@
*/
class RedisSessionTest extends TestCase
{
- public function testReadWrite()
+ public function testReadWrite(): void
{
$session = new Session();
@@ -29,7 +29,7 @@ public function testReadWrite()
*
* @runInSeparateProcess
*/
- public function testSetName()
+ public function testSetName(): void
{
$session = new Session();
$session->setName('oldName');
@@ -49,7 +49,7 @@ public function testSetName()
*
* @runInSeparateProcess
*/
- public function testStrictMode()
+ public function testStrictMode(): void
{
//non-strict-mode test
$nonStrictSession = new Session([
diff --git a/tests/TestCase.php b/tests/TestCase.php
index c79ae14..be35e30 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -24,7 +24,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
*
* @return mixed the value of the configuration param
*/
- public static function getParam($name, $default = null)
+ public static function getParam($name, mixed $default = null)
{
if (static::$params === null) {
static::$params = require(__DIR__ . '/data/config.php');
@@ -62,7 +62,6 @@ protected function mockApplication(array $config = [], $appClass = '\yii\console
/**
* Mocks web application
*
- * @param array $config
* @param string $appClass
*/
protected function mockWebApplication(array $config = [], $appClass = '\yii\web\Application')
diff --git a/tests/UniqueValidatorTest.php b/tests/UniqueValidatorTest.php
index 8ac4824..2555bb4 100644
--- a/tests/UniqueValidatorTest.php
+++ b/tests/UniqueValidatorTest.php
@@ -14,7 +14,7 @@
*/
class UniqueValidatorTest extends TestCase
{
- public function testValidationInsert()
+ public function testValidationInsert(): void
{
ActiveRecord::$db = $this->getConnection(true);
@@ -36,7 +36,7 @@ public function testValidationInsert()
$this->assertTrue($customer->hasErrors('email'));
}
- public function testValidationUpdate()
+ public function testValidationUpdate(): void
{
ActiveRecord::$db = $this->getConnection(true);
@@ -60,7 +60,7 @@ public function testValidationUpdate()
$this->assertTrue($customer1->hasErrors('email'));
}
- public function testValidationInsertCompositePk()
+ public function testValidationInsertCompositePk(): void
{
ActiveRecord::$db = $this->getConnection(true);
@@ -83,7 +83,7 @@ public function testValidationInsertCompositePk()
$this->assertTrue($model->hasErrors('item_id'));
}
- public function testValidationInsertCompositePkUniqueAttribute()
+ public function testValidationInsertCompositePkUniqueAttribute(): void
{
ActiveRecord::$db = $this->getConnection(true);
@@ -105,7 +105,7 @@ public function testValidationInsertCompositePkUniqueAttribute()
$this->assertTrue($model->hasErrors('quantity'));
}
- public function testValidationUpdateCompositePk()
+ public function testValidationUpdateCompositePk(): void
{
ActiveRecord::$db = $this->getConnection(true);
@@ -130,7 +130,7 @@ public function testValidationUpdateCompositePk()
$this->assertTrue($model1->hasErrors('item_id'));
}
- public function testValidationUpdateCompositePkUniqueAttribute()
+ public function testValidationUpdateCompositePkUniqueAttribute(): void
{
ActiveRecord::$db = $this->getConnection(true);
diff --git a/tests/data/ar/ActiveRecordTestTrait.php b/tests/data/ar/ActiveRecordTestTrait.php
index 7dd15d1..6cd0190 100644
--- a/tests/data/ar/ActiveRecordTestTrait.php
+++ b/tests/data/ar/ActiveRecordTestTrait.php
@@ -59,11 +59,11 @@ abstract public function getOrderItemWithNullFKmClass();
/**
* Can be overridden to do things after save().
*/
- public function afterSave()
+ public function afterSave(): void
{
}
- public function testFind()
+ public function testFind(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -126,7 +126,7 @@ public function testFind()
$this->assertEquals(2, $customerClass::find()->active()->count());
}
- public function testFindAsArray()
+ public function testFindAsArray(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -162,7 +162,7 @@ public function testFindAsArray()
$this->assertArrayHasKey('status', $customers[2]);
}
- public function testHasAttribute()
+ public function testHasAttribute(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -182,7 +182,7 @@ public function testHasAttribute()
$this->assertFalse($customer->hasAttribute(42));
}
- public function testFindScalar()
+ public function testFindScalar(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -199,7 +199,7 @@ public function testFindScalar()
$this->assertEquals(3, $customerId);
}
- public function testFindColumn()
+ public function testFindColumn(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -209,7 +209,7 @@ public function testFindColumn()
$this->assertEquals(['user3', 'user2', 'user1'], $customerClass::find()->orderBy(['name' => SORT_DESC])->column('name'));
}
- public function testFindIndexBy()
+ public function testFindIndexBy(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -222,16 +222,14 @@ public function testFindIndexBy()
$this->assertInstanceOf($customerClass, $customers['user3']);
// indexBy callable
- $customers = $customerClass::find()->indexBy(function ($customer) {
- return $customer->id . '-' . $customer->name;
- })->orderBy('id')->all();
+ $customers = $customerClass::find()->indexBy(fn($customer) => $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()
+ public function testFindIndexByAsArray(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -257,9 +255,7 @@ public function testFindIndexByAsArray()
$this->assertArrayHasKey('status', $customers['user3']);
// indexBy callable + asArray
- $customers = $customerClass::find()->indexBy(function ($customer) {
- return $customer['id'] . '-' . $customer['name'];
- })->asArray()->all();
+ $customers = $customerClass::find()->indexBy(fn($customer) => $customer['id'] . '-' . $customer['name'])->asArray()->all();
$this->assertCount(3, $customers);
$this->assertArrayHasKey('id', $customers['1-user1']);
$this->assertArrayHasKey('name', $customers['1-user1']);
@@ -278,7 +274,7 @@ public function testFindIndexByAsArray()
$this->assertArrayHasKey('status', $customers['3-user3']);
}
- public function testRefresh()
+ public function testRefresh(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -292,7 +288,7 @@ public function testRefresh()
$this->assertEquals('user1', $customer->name);
}
- public function testEquals()
+ public function testEquals(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -320,7 +316,7 @@ public function testEquals()
$this->assertFalse($customerA->equals($customerB));
}
- public function testFindCount()
+ public function testFindCount(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -340,7 +336,7 @@ public function testFindCount()
$this->assertEquals(3, $customerClass::find()->offset(2)->limit(2)->count());
}
- public function testFindLimit()
+ public function testFindLimit(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -387,7 +383,7 @@ public function testFindLimit()
$this->assertNull($customer);
}
- public function testFindComplexCondition()
+ public function testFindComplexCondition(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -403,7 +399,7 @@ public function testFindComplexCondition()
$this->assertCount(1, $customerClass::find()->where(['AND', ['name' => ['user2', 'user3']], ['BETWEEN', 'status', 2, 4]])->all());
}
- public function testFindNullValues()
+ public function testFindNullValues(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -419,7 +415,7 @@ public function testFindNullValues()
$this->assertEquals(2, reset($result)->primaryKey);
}
- public function testExists()
+ public function testExists(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -435,7 +431,7 @@ public function testExists()
$this->assertFalse($customerClass::find()->where(['id' => [2, 3]])->offset(2)->exists());
}
- public function testFindLazy()
+ public function testFindLazy(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -463,7 +459,7 @@ public function testFindLazy()
$this->assertEquals(3, $orders[0]->id);
}
- public function testFindEager()
+ public function testFindEager(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -500,7 +496,7 @@ public function testFindEager()
$this->assertTrue($orders[0]->isRelationPopulated('items'));
}
- public function testFindLazyVia()
+ public function testFindLazyVia(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -514,7 +510,7 @@ public function testFindLazyVia()
$this->assertEquals(2, $order->items[1]->id);
}
- public function testFindLazyVia2()
+ public function testFindLazyVia2(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -526,7 +522,7 @@ public function testFindLazyVia2()
$this->assertEquals([], $order->items);
}
- public function testFindEagerViaRelation()
+ public function testFindEagerViaRelation(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -542,7 +538,7 @@ public function testFindEagerViaRelation()
$this->assertEquals(2, $order->items[1]->id);
}
- public function testFindNestedRelation()
+ public function testFindNestedRelation(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -579,7 +575,7 @@ public function testFindNestedRelation()
*
* @see https://github.com/yiisoft/yii2/issues/1310.
*/
- public function testFindEagerViaRelationPreserveOrder()
+ public function testFindEagerViaRelationPreserveOrder(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -642,7 +638,7 @@ public function testFindEagerViaRelationPreserveOrder()
}
// different order in via table
- public function testFindEagerViaRelationPreserveOrderB()
+ public function testFindEagerViaRelationPreserveOrderB(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
@@ -672,7 +668,7 @@ public function testFindEagerViaRelationPreserveOrderB()
$this->assertEquals(2, $order->itemsInOrder2[0]->id);
}
- public function testLink()
+ public function testLink(): void
{
/* @var $orderClass ActiveRecordInterface */
/* @var $itemClass ActiveRecordInterface */
@@ -725,7 +721,7 @@ public function testLink()
$this->assertEquals(100, $orderItem->subtotal);
}
- public function testUnlink()
+ public function testUnlink(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -776,7 +772,7 @@ public function testUnlink()
$this->assertCount(2, $order->orderItems);
}
- public function testUnlinkAll()
+ public function testUnlinkAll(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -842,7 +838,7 @@ public function testUnlinkAll()
// via table is covered in \yiiunit\framework\db\ActiveRecordTest::testUnlinkAllViaTable()
}
- public function testUnlinkAllAndConditionSetNull()
+ public function testUnlinkAllAndConditionSetNull(): void
{
/* @var $this TestCase|ActiveRecordTestTrait */
@@ -868,7 +864,7 @@ public function testUnlinkAllAndConditionSetNull()
$this->assertCount(0, $customer->expensiveOrdersWithNullFK);
}
- public function testUnlinkAllAndConditionDelete()
+ public function testUnlinkAllAndConditionDelete(): void
{
/* @var $this TestCase|ActiveRecordTestTrait */
@@ -897,7 +893,7 @@ public function testUnlinkAllAndConditionDelete()
public static $afterSaveNewRecord;
public static $afterSaveInsert;
- public function testInsert()
+ public function testInsert(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -921,7 +917,7 @@ public function testInsert()
$this->assertFalse($customer->isNewRecord);
}
- public function testExplicitPkOnAutoIncrement()
+ public function testExplicitPkOnAutoIncrement(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -940,7 +936,7 @@ public function testExplicitPkOnAutoIncrement()
$this->assertFalse($customer->isNewRecord);
}
- public function testUpdate()
+ public function testUpdate(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -983,7 +979,7 @@ public function testUpdate()
$this->assertEquals(0, $ret);
}
- public function testUpdateAttributes()
+ public function testUpdateAttributes(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1018,7 +1014,7 @@ public function testUpdateAttributes()
$this->assertEquals(1, $customer->status);
}
- public function testUpdateCounters()
+ public function testUpdateCounters(): void
{
/* @var $orderItemClass ActiveRecordInterface */
$orderItemClass = $this->getOrderItemClass();
@@ -1049,7 +1045,7 @@ public function testUpdateCounters()
$this->assertEquals(30, $orderItem->subtotal);
}
- public function testDelete()
+ public function testDelete(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1081,7 +1077,7 @@ public function testDelete()
* Some PDO implementations do not support boolean values.
* Make sure this does not affect AR layer.
*/
- public function testBooleanAttribute()
+ public function testBooleanAttribute(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1108,7 +1104,7 @@ public function testBooleanAttribute()
$this->assertCount(1, $customers);
}
- public function testAfterFind()
+ public function testAfterFind(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1117,10 +1113,10 @@ public function testAfterFind()
/* @var $this TestCase|ActiveRecordTestTrait */
$afterFindCalls = [];
- Event::on(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) {
+ Event::on(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls): void {
/* @var $ar BaseActiveRecord */
$ar = $event->sender;
- $afterFindCalls[] = [\get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')];
+ $afterFindCalls[] = [$ar::class, $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')];
});
$customer = $customerClass::findOne(1);
@@ -1165,17 +1161,17 @@ public function testAfterFind()
Event::off(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_FIND);
}
- public function testAfterRefresh()
+ public function testAfterRefresh(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
/* @var $this TestCase|ActiveRecordTestTrait */
$afterRefreshCalls = [];
- Event::on(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_REFRESH, function ($event) use (&$afterRefreshCalls) {
+ Event::on(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_REFRESH, function ($event) use (&$afterRefreshCalls): void {
/* @var $ar BaseActiveRecord */
$ar = $event->sender;
- $afterRefreshCalls[] = [\get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')];
+ $afterRefreshCalls[] = [$ar::class, $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')];
});
$customer = $customerClass::findOne(1);
@@ -1186,7 +1182,7 @@ public function testAfterRefresh()
Event::off(BaseActiveRecord::class, BaseActiveRecord::EVENT_AFTER_REFRESH);
}
- public function testFindEmptyInCondition()
+ public function testFindEmptyInCondition(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1205,7 +1201,7 @@ public function testFindEmptyInCondition()
$this->assertCount(0, $customers);
}
- public function testFindEagerIndexBy()
+ public function testFindEagerIndexBy(): void
{
/* @var $this TestCase|ActiveRecordTestTrait */
@@ -1230,7 +1226,7 @@ public function testFindEagerIndexBy()
$this->assertTrue(isset($items[5]));
}
- public function testAttributeAccess()
+ public function testAttributeAccess(): void
{
/* @var $customerClass ActiveRecordInterface */
$customerClass = $this->getCustomerClass();
@@ -1294,7 +1290,7 @@ public function testAttributeAccess()
/**
* @see https://github.com/yiisoft/yii2/issues/17089
*/
- public function testViaWithCallable()
+ public function testViaWithCallable(): void
{
/* @var $orderClass ActiveRecordInterface */
$orderClass = $this->getOrderClass();
diff --git a/tests/data/ar/Customer.php b/tests/data/ar/Customer.php
index 78a1aec..63970ef 100644
--- a/tests/data/ar/Customer.php
+++ b/tests/data/ar/Customer.php
@@ -25,8 +25,8 @@
*/
class Customer extends ActiveRecord
{
- public const STATUS_ACTIVE = 1;
- public const STATUS_INACTIVE = 2;
+ final public const STATUS_ACTIVE = 1;
+ final public const STATUS_INACTIVE = 2;
public $status2;
@@ -89,7 +89,7 @@ public function getOrderItems()
/**
* @inheritdoc
*/
- public function afterSave($insert, $changedAttributes)
+ public function afterSave($insert, $changedAttributes): void
{
ActiveRecordTest::$afterSaveInsert = $insert;
ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord;
diff --git a/tests/data/ar/Order.php b/tests/data/ar/Order.php
index f30f9f6..a0f33d8 100644
--- a/tests/data/ar/Order.php
+++ b/tests/data/ar/Order.php
@@ -56,7 +56,7 @@ public function getOrderItems()
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
- ->via('orderItems', function ($q) {
+ ->via('orderItems', function ($q): void {
// additional query configuration
});
}
@@ -64,7 +64,7 @@ public function getItems()
public function getExpensiveItemsUsingViaWithCallable()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
- ->via('orderItems', function (\yii\redis\ActiveQuery $q) {
+ ->via('orderItems', function (\yii\redis\ActiveQuery $q): void {
$q->where(['>=', 'subtotal', 10]);
});
}
@@ -72,7 +72,7 @@ public function getExpensiveItemsUsingViaWithCallable()
public function getCheapItemsUsingViaWithCallable()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
- ->via('orderItems', function (\yii\redis\ActiveQuery $q) {
+ ->via('orderItems', function (\yii\redis\ActiveQuery $q): void {
$q->where(['<', 'subtotal', 10]);
});
}
@@ -109,7 +109,7 @@ public function getOrderItemsWithNullFK()
public function getItemsInOrder1()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
- ->via('orderItems', function ($q) {
+ ->via('orderItems', function ($q): void {
$q->orderBy(['subtotal' => SORT_ASC]);
})->orderBy('name');
}
@@ -120,7 +120,7 @@ public function getItemsInOrder1()
public function getItemsInOrder2()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
- ->via('orderItems', function ($q) {
+ ->via('orderItems', function ($q): void {
$q->orderBy(['subtotal' => SORT_DESC]);
})->orderBy('name');
}
From ceb5d8f7101f47a59137b78a8838126094b580ca Mon Sep 17 00:00:00 2001
From: StyleCI Bot
Date: Tue, 17 Oct 2023 20:50:18 +0000
Subject: [PATCH 23/25] Apply fixes from StyleCI
---
tests/RedisConnectionTest.php | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/tests/RedisConnectionTest.php b/tests/RedisConnectionTest.php
index b4bad8d..96503dd 100644
--- a/tests/RedisConnectionTest.php
+++ b/tests/RedisConnectionTest.php
@@ -64,7 +64,6 @@ public function testReConnect(): void
/**
* @dataProvider \yiiunit\extensions\redis\providers\Data::keyValueData
- *
*/
public function testStoreGet(mixed $data): void
{
@@ -262,7 +261,7 @@ public function testHMSet($params, $pairs): void
{
$redis = $this->getConnection();
$set = $params[0];
- call_user_func_array($redis->hmset(...), $params);
+ ($redis->hmset(...))(...$params);
foreach ($pairs as $field => $expected) {
$actual = $redis->hget($set, $field);
$this->assertEquals($expected, $actual);
From 6ef5fee9638b83fcf16bae35aae2ff2a4fcaa288 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 17:53:46 -0300
Subject: [PATCH 24/25] Update workflow.
---
.github/workflows/static.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index c8974c7..75a378c 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -22,7 +22,7 @@ on:
name: static analysis
jobs:
- psalm:
+ phpstan:
uses: php-forge/actions/.github/workflows/phpstan.yml@main
with:
os: >-
From 5851ef899cf9d23a10a0804392aeec49c605e210 Mon Sep 17 00:00:00 2001
From: Wilmer Arambula
Date: Tue, 17 Oct 2023 18:00:58 -0300
Subject: [PATCH 25/25] Update `README.md`.
---
README.md | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 7fc0a59..360332b 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
-
+
-
Yii2-Template.
-
+ Redis
@@ -13,21 +12,21 @@
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
## Requirements