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 @@ yii2-version - - PHPUnit + + PHPUnit - - Codecov + + Codecov + + + PHPStan - - PHPStan + + PHPStan level - - PHPStan level + + Code style - - Code style -

## Requirements