diff --git a/apps/advanced/backend/config/main.php b/apps/advanced/backend/config/main.php index 41261f6..a0b5fc9 100644 --- a/apps/advanced/backend/config/main.php +++ b/apps/advanced/backend/config/main.php @@ -13,10 +13,6 @@ return [ 'bootstrap' => ['log'], 'modules' => [], 'components' => [ - 'request' => [ - // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation - 'cookieValidationKey' => '', - ], 'user' => [ 'identityClass' => 'common\models\User', 'enableAutoLogin' => true, diff --git a/apps/advanced/composer.json b/apps/advanced/composer.json index eb6d7fe..639396b 100644 --- a/apps/advanced/composer.json +++ b/apps/advanced/composer.json @@ -32,8 +32,7 @@ }, "scripts": { "post-create-project-cmd": [ - "yii\\composer\\Installer::setPermission", - "yii\\composer\\Installer::generateCookieValidationKey" + "yii\\composer\\Installer::setPermission" ] }, "config": { @@ -46,10 +45,6 @@ "frontend/runtime", "frontend/web/assets" - ], - "config": [ - "frontend/config/main.php", - "backend/config/main.php" ] } } diff --git a/apps/advanced/environments/dev/backend/config/main-local.php b/apps/advanced/environments/dev/backend/config/main-local.php index 0e6a38e..d9e3809 100644 --- a/apps/advanced/environments/dev/backend/config/main-local.php +++ b/apps/advanced/environments/dev/backend/config/main-local.php @@ -1,6 +1,13 @@ <?php -$config = []; +$config = [ + 'components' => [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; if (!YII_ENV_TEST) { // configuration adjustments for 'dev' environment diff --git a/apps/advanced/environments/dev/frontend/config/main-local.php b/apps/advanced/environments/dev/frontend/config/main-local.php index 0e6a38e..d9e3809 100644 --- a/apps/advanced/environments/dev/frontend/config/main-local.php +++ b/apps/advanced/environments/dev/frontend/config/main-local.php @@ -1,6 +1,13 @@ <?php -$config = []; +$config = [ + 'components' => [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], +]; if (!YII_ENV_TEST) { // configuration adjustments for 'dev' environment diff --git a/apps/advanced/environments/index.php b/apps/advanced/environments/index.php index a6fd1b7..e7ab700 100644 --- a/apps/advanced/environments/index.php +++ b/apps/advanced/environments/index.php @@ -9,9 +9,15 @@ * return [ * 'environment name' => [ * 'path' => 'directory storing the local files', - * 'writable' => [ + * 'setWritable' => [ * // list of directories that should be set writable * ], + * 'setExecutable' => [ + * // list of directories that should be set executable + * ], + * 'setCookieValidationKey' => [ + * // list of config files that need to be inserted with automatically generated cookie validation keys + * ], * ], * ]; * ``` @@ -19,26 +25,34 @@ return [ 'Development' => [ 'path' => 'dev', - 'writable' => [ + 'setWritable' => [ 'backend/runtime', 'backend/web/assets', 'frontend/runtime', 'frontend/web/assets', ], - 'executable' => [ + 'setExecutable' => [ 'yii', ], + 'setCookieValidationKey' => [ + 'backend/config/main-local.php', + 'frontend/config/main-local.php', + ], ], 'Production' => [ 'path' => 'prod', - 'writable' => [ + 'setWritable' => [ 'backend/runtime', 'backend/web/assets', 'frontend/runtime', 'frontend/web/assets', ], - 'executable' => [ + 'setExecutable' => [ 'yii', ], + 'setCookieValidationKey' => [ + 'backend/config/main-local.php', + 'frontend/config/main-local.php', + ], ], ]; diff --git a/apps/advanced/environments/prod/backend/config/main-local.php b/apps/advanced/environments/prod/backend/config/main-local.php index d0b9c34..af46ba3 100644 --- a/apps/advanced/environments/prod/backend/config/main-local.php +++ b/apps/advanced/environments/prod/backend/config/main-local.php @@ -1,3 +1,9 @@ <?php return [ + 'components' => [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], ]; diff --git a/apps/advanced/environments/prod/frontend/config/main-local.php b/apps/advanced/environments/prod/frontend/config/main-local.php index d0b9c34..af46ba3 100644 --- a/apps/advanced/environments/prod/frontend/config/main-local.php +++ b/apps/advanced/environments/prod/frontend/config/main-local.php @@ -1,3 +1,9 @@ <?php return [ + 'components' => [ + 'request' => [ + // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation + 'cookieValidationKey' => '', + ], + ], ]; diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php index 1e442b3..1ed8305 100644 --- a/apps/advanced/frontend/config/main.php +++ b/apps/advanced/frontend/config/main.php @@ -12,10 +12,6 @@ return [ 'bootstrap' => ['log'], 'controllerNamespace' => 'frontend\controllers', 'components' => [ - 'request' => [ - // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation - 'cookieValidationKey' => '', - ], 'user' => [ 'identityClass' => 'common\models\User', 'enableAutoLogin' => true, diff --git a/apps/advanced/init b/apps/advanced/init index 4858321..db9dd7e 100755 --- a/apps/advanced/init +++ b/apps/advanced/init @@ -14,6 +14,10 @@ * @license http://www.yiiframework.com/license/ */ +if (!extension_loaded('mcrypt')) { + die('The mcrypt PHP extension is required by Yii2.'); +} + $params = getParams(); $root = str_replace('\\', '/', __DIR__); $envs = require("$root/environments/index.php"); @@ -23,147 +27,169 @@ echo "Yii Application Initialization Tool v1.0\n\n"; $envName = null; if (empty($params['env']) || $params['env'] === '1') { - echo "Which environment do you want the application to be initialized in?\n\n"; - foreach ($envNames as $i => $name) { - echo " [$i] $name\n"; - } - echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; - $answer = trim(fgets(STDIN)); - - if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { - echo "\n Quit initialization.\n"; - exit(0); - } - - if (isset($envNames[$answer])) { - $envName = $envNames[$answer]; - } + echo "Which environment do you want the application to be initialized in?\n\n"; + foreach ($envNames as $i => $name) { + echo " [$i] $name\n"; + } + echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; + $answer = trim(fgets(STDIN)); + + if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { + echo "\n Quit initialization.\n"; + exit(0); + } + + if (isset($envNames[$answer])) { + $envName = $envNames[$answer]; + } } else { - $envName = $params['env']; + $envName = $params['env']; } if (!in_array($envName, $envNames)) { - $envsList = implode(', ', $envNames); - echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n"; - exit(2); + $envsList = implode(', ', $envNames); + echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n"; + exit(2); } $env = $envs[$envName]; if (empty($params['env'])) { - echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; - $answer = trim(fgets(STDIN)); - if (strncasecmp($answer, 'y', 1)) { - echo "\n Quit initialization.\n"; - exit(0); - } + echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; + $answer = trim(fgets(STDIN)); + if (strncasecmp($answer, 'y', 1)) { + echo "\n Quit initialization.\n"; + exit(0); + } } echo "\n Start initialization ...\n\n"; $files = getFileList("$root/environments/{$env['path']}"); $all = false; foreach ($files as $file) { - if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) { - break; - } -} - -if (isset($env['writable'])) { - foreach ($env['writable'] as $writable) { - echo " chmod 0777 $writable\n"; - @chmod("$root/$writable", 0777); - } + if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) { + break; + } } -if (isset($env['executable'])) { - foreach ($env['executable'] as $executable) { - echo " chmod 0755 $executable\n"; - @chmod("$root/$executable", 0755); - } +$callbacks = ['setCookieValidationKey', 'setWritable', 'setExecutable']; +foreach ($callbacks as $callback) { + if (!empty($env[$callback])) { + $callback($root, $env[$callback]); + } } echo "\n ... initialization completed.\n\n"; function getFileList($root, $basePath = '') { - $files = []; - $handle = opendir($root); - while (($path = readdir($handle)) !== false) { - if ($path === '.svn' || $path === '.' || $path === '..') { - continue; - } - $fullPath = "$root/$path"; - $relativePath = $basePath === '' ? $path : "$basePath/$path"; - if (is_dir($fullPath)) { - $files = array_merge($files, getFileList($fullPath, $relativePath)); - } else { - $files[] = $relativePath; - } - } - closedir($handle); - return $files; + $files = []; + $handle = opendir($root); + while (($path = readdir($handle)) !== false) { + if ($path === '.svn' || $path === '.' || $path === '..') { + continue; + } + $fullPath = "$root/$path"; + $relativePath = $basePath === '' ? $path : "$basePath/$path"; + if (is_dir($fullPath)) { + $files = array_merge($files, getFileList($fullPath, $relativePath)); + } else { + $files[] = $relativePath; + } + } + closedir($handle); + return $files; } function copyFile($root, $source, $target, &$all, $params) { - if (!is_file($root . '/' . $source)) { - echo " skip $target ($source not exist)\n"; - return true; - } - if (is_file($root . '/' . $target)) { - if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) { - echo " unchanged $target\n"; - return true; - } - if ($all) { - echo " overwrite $target\n"; - } else { - echo " exist $target\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - - - $answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN)); - if (!strncasecmp($answer, 'q', 1)) { - return false; - } else { - if (!strncasecmp($answer, 'y', 1)) { - echo " overwrite $target\n"; - } else { - if (!strncasecmp($answer, 'a', 1)) { - echo " overwrite $target\n"; - $all = true; - } else { - echo " skip $target\n"; - return true; - } - } - } - } - file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); - return true; - } - echo " generate $target\n"; - @mkdir(dirname($root . '/' . $target), 0777, true); - file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); - return true; + if (!is_file($root . '/' . $source)) { + echo " skip $target ($source not exist)\n"; + return true; + } + if (is_file($root . '/' . $target)) { + if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) { + echo " unchanged $target\n"; + return true; + } + if ($all) { + echo " overwrite $target\n"; + } else { + echo " exist $target\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + + + $answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN)); + if (!strncasecmp($answer, 'q', 1)) { + return false; + } else { + if (!strncasecmp($answer, 'y', 1)) { + echo " overwrite $target\n"; + } else { + if (!strncasecmp($answer, 'a', 1)) { + echo " overwrite $target\n"; + $all = true; + } else { + echo " skip $target\n"; + return true; + } + } + } + } + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; + } + echo " generate $target\n"; + @mkdir(dirname($root . '/' . $target), 0777, true); + file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); + return true; } function getParams() { - $rawParams = []; - if (isset($_SERVER['argv'])) { - $rawParams = $_SERVER['argv']; - array_shift($rawParams); - } - - $params = []; - foreach ($rawParams as $param) { - if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { - $name = $matches[1]; - $params[$name] = isset($matches[3]) ? $matches[3] : true; - } else { - $params[] = $param; - } - } - return $params; + $rawParams = []; + if (isset($_SERVER['argv'])) { + $rawParams = $_SERVER['argv']; + array_shift($rawParams); + } + + $params = []; + foreach ($rawParams as $param) { + if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { + $name = $matches[1]; + $params[$name] = isset($matches[3]) ? $matches[3] : true; + } else { + $params[] = $param; + } + } + return $params; +} + +function setWritable($root, $paths) +{ + foreach ($paths as $writable) { + echo " chmod 0777 $writable\n"; + @chmod("$root/$writable", 0777); + } +} + +function setExecutable($root, $paths) +{ + foreach ($paths as $executable) { + echo " chmod 0755 $executable\n"; + @chmod("$root/$executable", 0755); + } +} + +function setCookieValidationKey($root, $paths) +{ + foreach ($paths as $file) { + echo " generate cookie validation key in $file\n"; + $file = $root . '/' . $file; + $length = 32; + $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + $key = strtr(substr(base64_encode($bytes), 0, $length), '+/=', '_-.'); + $content = preg_replace('/(("|\')cookieValidationKey("|\')\s*=>\s*)(""|\'\')/', "\\1'$key'", file_get_contents($file)); + file_put_contents($file, $content); + } } diff --git a/docs/guide/db-migrations.md b/docs/guide/db-migrations.md index c591fa4..adc9751 100644 --- a/docs/guide/db-migrations.md +++ b/docs/guide/db-migrations.md @@ -97,7 +97,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration } ``` -The base class [\yii\db\Migration] exposes a database connection via `db` +The base class [[\yii\db\Migration]] exposes a database connection via `db` property. You can use it for manipulating data and schema of a database. The column types used in this example are abstract types that will be replaced diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index 23329e5..64b0131 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -249,6 +249,19 @@ Operator can be one of the following: - `not exists`: similar to the `exists` operator and builds a `NOT EXISTS (sub-query)` expression. +Additionally you can specify anything as operator: + +```php +$userQuery = (new Query)->select('id')->from('user'); +$query->where(['>=', 'id', 10]); +``` + +It will result in: + +```sql +SELECT id FROM user WHERE id >= 10; +``` + If you are building parts of condition dynamically it's very convenient to use `andWhere()` and `orWhere()`: ```php @@ -305,8 +318,6 @@ $query->orderBy([ Here we are ordering by `id` ascending and then by `name` descending. -``` - ### `GROUP BY` and `HAVING` In order to add `GROUP BY` to generated SQL you can use the following: diff --git a/docs/guide/tutorial-template-engines.md b/docs/guide/tutorial-template-engines.md index 4fc92db..3a7ad8b 100644 --- a/docs/guide/tutorial-template-engines.md +++ b/docs/guide/tutorial-template-engines.md @@ -56,7 +56,7 @@ return $this->render('renderer.twig', ['username' => 'Alex']); ### Template syntax The best resource to learn Twig basics is its official documentation you can find at -[twig.sensiolabs.org](http://twig.sensiolabs.org/documentation). Additionally there are Yii-specific addtions +[twig.sensiolabs.org](http://twig.sensiolabs.org/documentation). Additionally there are Yii-specific syntax extensions described below. #### Method and function calls @@ -271,7 +271,13 @@ or `$this->renderPartial()` controller calls: return $this->render('renderer.tpl', ['username' => 'Alex']); ``` -### Additional functions +### Template syntax + +The best resource to learn Smarty template syntax is its official documentation you can find at +[www.smarty.net](http://www.smarty.net/docs/en/). Additionally there are Yii-specific syntax extensions +described below. + +#### Additional functions Yii adds the following construct to the standard Smarty syntax: @@ -281,7 +287,7 @@ Yii adds the following construct to the standard Smarty syntax: Internally, the `path()` function calls Yii's `Url::to()` method. -### Additional variables +#### Additional variables Within Smarty templates, you can also make use of these variables: diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php index f48c18f..8a29795 100644 --- a/extensions/gii/generators/model/Generator.php +++ b/extensions/gii/generators/model/Generator.php @@ -197,7 +197,7 @@ class Generator extends \yii\gii\Generator $labels[$column->name] = 'ID'; } else { $label = Inflector::camel2words($column->name); - if (!empty($label) && substr_compare($label, ' id', -3, 3, true)) { + if (!empty($label) && substr_compare($label, ' id', -3, 3, true) === 0) { $label = substr($label, 0, -3) . ' ID'; } $labels[$column->name] = $label; @@ -508,16 +508,16 @@ class Generator extends \yii\gii\Generator } } - private $_tableNames; - private $_classNames; + protected $tableNames; + protected $classNames; /** * @return array the table names that match the pattern specified by [[tableName]]. */ protected function getTableNames() { - if ($this->_tableNames !== null) { - return $this->_tableNames; + if ($this->tableNames !== null) { + return $this->tableNames; } $db = $this->getDbConnection(); if ($db === null) { @@ -540,10 +540,10 @@ class Generator extends \yii\gii\Generator } } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { $tableNames[] = $this->tableName; - $this->_classNames[$this->tableName] = $this->modelClass; + $this->classNames[$this->tableName] = $this->modelClass; } - return $this->_tableNames = $tableNames; + return $this->tableNames = $tableNames; } /** @@ -574,8 +574,8 @@ class Generator extends \yii\gii\Generator */ protected function generateClassName($tableName) { - if (isset($this->_classNames[$tableName])) { - return $this->_classNames[$tableName]; + if (isset($this->classNames[$tableName])) { + return $this->classNames[$tableName]; } if (($pos = strrpos($tableName, '.')) !== false) { @@ -601,7 +601,7 @@ class Generator extends \yii\gii\Generator } } - return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); + return $this->classNames[$tableName] = Inflector::id2camel($className, '_'); } /** diff --git a/extensions/sphinx/QueryBuilder.php b/extensions/sphinx/QueryBuilder.php index f1b434a..cf63ab3 100644 --- a/extensions/sphinx/QueryBuilder.php +++ b/extensions/sphinx/QueryBuilder.php @@ -617,12 +617,11 @@ class QueryBuilder extends Object $operator = strtoupper($condition[0]); if (isset($builders[$operator])) { $method = $builders[$operator]; - array_shift($condition); - - return $this->$method($indexes, $operator, $condition, $params); } else { - throw new Exception('Found unknown operator in query: ' . $operator); + $method = 'buildSimpleCondition'; } + array_shift($condition); + return $this->$method($indexes, $operator, $condition, $params); } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... return $this->buildHashCondition($indexes, $condition, $params); @@ -986,4 +985,29 @@ class QueryBuilder extends Object return $phName; } } + + /** + * Creates an SQL expressions like `"column" operator value`. + * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands contains two column names. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildSimpleCondition($operator, $operands, &$params) + { + if (count($operands) !== 2) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + list($column, $value) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value === null ? 'NULL' : $value; + + return "$column $operator $phName"; + } } diff --git a/extensions/twig/ViewRenderer.php b/extensions/twig/ViewRenderer.php index 8f634a6..c9ccbe2 100644 --- a/extensions/twig/ViewRenderer.php +++ b/extensions/twig/ViewRenderer.php @@ -144,16 +144,30 @@ class ViewRenderer extends BaseViewRenderer { $this->twig->addGlobal('this', $view); $loader = new \Twig_Loader_Filesystem(dirname($file)); - - foreach (Yii::$aliases as $alias => $path) { - $loader->addPath($path, substr($alias, 1)); - } + $this->addAliases($loader, Yii::$aliases); $this->twig->setLoader($loader); return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params); } /** + * Adds aliases + * + * @param \Twig_Loader_Filesystem $loader + * @param array $aliases + */ + protected function addAliases($loader, $aliases) + { + foreach ($aliases as $alias => $path) { + if (is_array($path)) { + $this->addAliases($loader, $path); + } elseif (is_string($path) && is_dir($path)) { + $loader->addPath($path, substr($alias, 1)); + } + } + } + + /** * Adds global objects or static classes * @param array $globals @see self::$globals */ diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e0e8a35..65bdc05 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -90,6 +90,7 @@ Yii Framework 2 Change Log - Enh #1388: Added mapping from physical types to abstract types for OCI DB driver (qiangxue) - Enh #1452: Added `Module::getInstance()` to allow accessing the module instance from anywhere within the module (qiangxue) - Enh #2264: `CookieCollection::has()` will return false for expired or removed cookies (qiangxue) +- Enh #2315: Any operator now could be used with `yii\db\Query::->where()` operand format (samdark) - Enh #2435: `yii\db\IntegrityException` is now thrown on database integrity errors instead of general `yii\db\Exception` (samdark) - Enh #2558: Enhanced support for memcached by adding `yii\caching\MemCache::persistentId` and `yii\caching\MemCache::options` (qiangxue) - Enh #2837: Error page now shows arguments in stack trace method calls (samdark) @@ -167,6 +168,7 @@ Yii Framework 2 Change Log - Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma) - Enh #4485: Added support for deferred validation in `ActiveForm` (Alex-Code) - Enh #4520: Added sasl support to `yii\caching\MemCache` (xjflyttp) +- Enh #4566: Added client validation support for image validator (Skysplit, qiangxue) - Enh: Added support for using sub-queries when building a DB query with `IN` condition (qiangxue) - Enh: Supported adding a new response formatter without the need to reconfigure existing formatters (qiangxue) - Enh: Added `yii\web\UrlManager::addRules()` to simplify adding new URL rules (qiangxue) diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index 69c0742..0da298e 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -68,55 +68,67 @@ yii.validation = (function ($) { pub.addMessage(messages, options.notEqual, value); } }, - - file: function (value, messages, options, attribute) { - var files = $(attribute.input).get(0).files, - index, ext; - - if (options.message && !files) { - pub.addMessage(messages, options.message, value); - } - - if (!options.skipOnEmpty && files.length == 0) { - pub.addMessage(messages, options.uploadRequired, value); - } else if (files.length == 0) { - return; - } - - if (options.maxFiles && options.maxFiles < files.length) { - pub.addMessage(messages, options.tooMany); - } - + + file: function (attribute, messages, options) { + var files = getUploadedFiles(attribute, messages, options); $.each(files, function (i, file) { - if (options.extensions && options.extensions.length > 0) { - index = file.name.lastIndexOf('.'); - - if (!~index) { - ext = ''; - } else { - ext = file.name.substr(index + 1, file.name.length).toLowerCase(); - } + validateFile(file, messages, options); + }); + }, + + image: function (attribute, messages, options, deferred) { + var files = getUploadedFiles(attribute, messages, options); + + $.each(files, function (i, file) { + validateFile(file, messages, options); - if (!~options.extensions.indexOf(ext)) { - messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); - } + // Skip image validation if FileReader API is not available + if (typeof FileReader === "undefined") { + return; } - if (options.mimeTypes && options.mimeTypes.length > 0) { - if (!~options.mimeTypes.indexOf(file.type)) { - messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name)); + var def = $.Deferred(), + fr = new FileReader(), + img = new Image(); + + img.onload = function () { + if (options.minWidth && this.width < options.minWidth) { + messages.push(options.underWidth.replace(/\{file\}/g, file.name)); } - } - - if (options.maxSize && options.maxSize < file.size) { - messages.push(options.tooBig.replace(/\{file\}/g, file.name)); - } - - if (options.maxSize && options.minSize > file.size) { - messages.push(options.tooSmall.replace(/\{file\}/g, file.name)); - } - + + if (options.maxWidth && this.width > options.maxWidth) { + messages.push(options.overWidth.replace(/\{file\}/g, file.name)); + } + + if (options.minHeight && this.height < options.minHeight) { + messages.push(options.underHeight.replace(/\{file\}/g, file.name)); + } + + if (options.maxHeight && this.height > options.maxHeight) { + messages.push(options.overHeight.replace(/\{file\}/g, file.name)); + } + def.resolve(); + }; + + img.onerror = function () { + messages.push(options.notImage); + def.resolve(); + }; + + fr.onload = function () { + img.src = fr.result; + }; + + // Resolve deferred if there was error while reading data + fr.onerror = function () { + def.resolve(); + }; + + fr.readAsDataURL(file); + + deferred.push(def); }); + }, number: function (value, messages, options) { @@ -288,5 +300,60 @@ yii.validation = (function ($) { } } }; + + function getUploadedFiles(attribute, messages, options) { + var files = $(attribute.input).get(0).files; + if (!files) { + messages.push(options.message); + return []; + } + + if (files.length === 0) { + if (!options.skipOnEmpty) { + messages.push(options.uploadRequired); + } + return []; + } + + if (options.maxFiles && options.maxFiles < files.length) { + messages.push(options.tooMany); + return []; + } + + return files; + } + + function validateFile(file, messages, options) { + if (options.extensions && options.extensions.length > 0) { + var index, ext; + + index = file.name.lastIndexOf('.'); + + if (!~index) { + ext = ''; + } else { + ext = file.name.substr(index + 1, file.name.length).toLowerCase(); + } + + if (!~options.extensions.indexOf(ext)) { + messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); + } + } + + if (options.mimeTypes && options.mimeTypes.length > 0) { + if (!~options.mimeTypes.indexOf(file.type)) { + messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name)); + } + } + + if (options.maxSize && options.maxSize < file.size) { + messages.push(options.tooBig.replace(/\{file\}/g, file.name)); + } + + if (options.minSize && options.minSize > file.size) { + messages.push(options.tooSmall.replace(/\{file\}/g, file.name)); + } + } + return pub; })(jQuery); diff --git a/framework/base/Model.php b/framework/base/Model.php index aaf3d5a..de8a024 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -385,7 +385,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab if ($this->_validators === null) { $this->_validators = $this->createValidators(); } - return $this->_validators; } @@ -404,7 +403,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab $validators[] = $validator; } } - return $validators; } @@ -427,7 +425,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); } } - return $validators; } @@ -436,6 +433,12 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab * This is determined by checking if the attribute is associated with a * [[\yii\validators\RequiredValidator|required]] validation rule in the * current [[scenario]]. + * + * Note that when the validator has a conditional validation applied using + * [[\yii\validators\RequiredValidator::$when|$when]] this method will return + * `false` regardless of the `when` condition because it may be called be + * before the model is loaded with data. + * * @param string $attribute attribute name * @return boolean whether the attribute is required */ @@ -446,7 +449,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab return true; } } - return false; } @@ -482,7 +484,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab public function getAttributeLabel($attribute) { $labels = $this->attributeLabels(); - return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); } diff --git a/framework/behaviors/AttributeBehavior.php b/framework/behaviors/AttributeBehavior.php index 1ec315d..3f2d007 100644 --- a/framework/behaviors/AttributeBehavior.php +++ b/framework/behaviors/AttributeBehavior.php @@ -93,7 +93,10 @@ class AttributeBehavior extends Behavior $attributes = (array) $this->attributes[$event->name]; $value = $this->getValue($event); foreach ($attributes as $attribute) { - $this->owner->$attribute = $value; + // ignore attribute names which are not string (e.g. when set by TimestampBehavior::updatedAtAttribute) + if (is_string($attribute)) { + $this->owner->$attribute = $value; + } } } } diff --git a/framework/behaviors/BlameableBehavior.php b/framework/behaviors/BlameableBehavior.php index 1b23577..791a077 100644 --- a/framework/behaviors/BlameableBehavior.php +++ b/framework/behaviors/BlameableBehavior.php @@ -54,10 +54,12 @@ class BlameableBehavior extends AttributeBehavior { /** * @var string the attribute that will receive current user ID value + * Set this property to be null if you do not want to record the creator ID. */ public $createdByAttribute = 'created_by'; /** * @var string the attribute that will receive current user ID value + * Set this property to be null if you do not want to record the updater ID. */ public $updatedByAttribute = 'updated_by'; /** diff --git a/framework/behaviors/TimestampBehavior.php b/framework/behaviors/TimestampBehavior.php index 9dc5348..8651d90 100644 --- a/framework/behaviors/TimestampBehavior.php +++ b/framework/behaviors/TimestampBehavior.php @@ -64,10 +64,12 @@ class TimestampBehavior extends AttributeBehavior { /** * @var string the attribute that will receive timestamp value + * Set this property to be null if you do not want to record the creation time. */ public $createdAtAttribute = 'created_at'; /** - * @var string the attribute that will receive timestamp value + * @var string the attribute that will receive timestamp value. + * Set this property to be null if you do not want to record the update time. */ public $updatedAtAttribute = 'updated_at'; /** diff --git a/framework/console/controllers/MessageController.php b/framework/console/controllers/MessageController.php index b97c970..1e1b5f1 100644 --- a/framework/console/controllers/MessageController.php +++ b/framework/console/controllers/MessageController.php @@ -96,6 +96,9 @@ class MessageController extends Controller if (!is_dir($config['sourcePath'])) { throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); } + if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) { + throw new Exception('Format should be either "php", "po" or "db".'); + } if (in_array($config['format'], ['php', 'po'])) { if (!isset($config['messagePath'])) { throw new Exception('The configuration file must specify "messagePath".'); @@ -106,9 +109,6 @@ class MessageController extends Controller if (empty($config['languages'])) { throw new Exception("Languages cannot be empty."); } - if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'db'])) { - throw new Exception('Format should be either "php", "po" or "db".'); - } $files = FileHelper::findFiles(realpath($config['sourcePath']), $config); diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 33068a5..b247981 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -868,7 +868,6 @@ class QueryBuilder extends \yii\base\Object * on how to specify a condition. * @param array $params the binding parameters to be populated * @return string the generated SQL expression - * @throws InvalidParamException if the condition is in bad format */ public function buildCondition($condition, &$params) { @@ -882,11 +881,11 @@ class QueryBuilder extends \yii\base\Object $operator = strtoupper($condition[0]); if (isset($this->conditionBuilders[$operator])) { $method = $this->conditionBuilders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition, $params); } else { - throw new InvalidParamException('Found unknown operator in query: ' . $operator); + $method = 'buildSimpleCondition'; } + array_shift($condition); + return $this->$method($operator, $condition, $params); } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... return $this->buildHashCondition($condition, $params); } @@ -1194,4 +1193,29 @@ class QueryBuilder extends \yii\base\Object throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.'); } } + + /** + * Creates an SQL expressions like `"column" operator value`. + * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands contains two column names. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildSimpleCondition($operator, $operands, &$params) + { + if (count($operands) !== 2) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + list($column, $value) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value === null ? 'NULL' : $value; + + return "$column $operator $phName"; + } } diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index 0244ff6..e700a2e 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -253,20 +253,6 @@ trait QueryTrait return []; } break; - case 'IN': - case 'NOT IN': - case 'LIKE': - case 'OR LIKE': - case 'NOT LIKE': - case 'OR NOT LIKE': - case 'ILIKE': // PostgreSQL operator for case insensitive LIKE - case 'OR ILIKE': - case 'NOT ILIKE': - case 'OR NOT ILIKE': - if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) { - return []; - } - break; case 'BETWEEN': case 'NOT BETWEEN': if (array_key_exists(1, $condition) && array_key_exists(2, $condition)) { @@ -276,7 +262,9 @@ trait QueryTrait } break; default: - throw new NotSupportedException("Operator not supported: $operator"); + if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) { + return []; + } } array_unshift($condition, $operator); diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index 9900a88..371e5d0 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -122,7 +122,7 @@ class FileValidator extends Validator * - {mimeTypes}: the value of [[mimeTypes]] */ public $wrongMimeType; - + /** * @inheritdoc @@ -150,12 +150,16 @@ class FileValidator extends Validator } if (!is_array($this->extensions)) { $this->extensions = preg_split('/[\s,]+/', strtolower($this->extensions), -1, PREG_SPLIT_NO_EMPTY); + } else { + $this->extensions = array_map('strtolower', $this->extensions); } if ($this->wrongMimeType === null) { $this->wrongMimeType = Yii::t('yii', 'Only files with these MIME types are allowed: {mimeTypes}.'); } if (!is_array($this->mimeTypes)) { $this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY); + } else { + $this->mimeTypes = array_map('strtolower', $this->mimeTypes); } } @@ -330,58 +334,77 @@ class FileValidator extends Validator /** * @inheritdoc */ - public function clientValidateAttribute($object, $attribute, $view) { + public function clientValidateAttribute($object, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($object, $attribute); + return 'yii.validation.file(attribute, messages, ' . json_encode($options) . ');'; + } + + /** + * Returns the client side validation options. + * @param \yii\base\Model $object the model being validated + * @param string $attribute the attribute name being validated + * @return array the client side validation options + */ + protected function getClientOptions($object, $attribute) + { $label = $object->getAttributeLabel($attribute); - + if ( $this->message !== null ){ $options['message'] = Yii::$app->getI18n()->format($this->message, [ 'attribute' => $label, ], Yii::$app->language); } - + $options['skipOnEmpty'] = $this->skipOnEmpty; - - if ( !$this->skipOnEmpty ) { - $options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [], Yii::$app->language); + + if ( !$this->skipOnEmpty ) { + $options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [ + 'attribute' => $label, + ], Yii::$app->language); } - + if ( $this->mimeTypes !== null ) { $options['mimeTypes'] = $this->mimeTypes; $options['wrongMimeType'] = Yii::$app->getI18n()->format($this->wrongMimeType, [ + 'attribute' => $label, 'mimeTypes' => join(', ', $this->mimeTypes) ], Yii::$app->language); } - + if ( $this->extensions !== null ) { $options['extensions'] = $this->extensions; $options['wrongExtension'] = Yii::$app->getI18n()->format($this->wrongExtension, [ + 'attribute' => $label, 'extensions' => join(', ', $this->extensions) ], Yii::$app->language); } - + if ( $this->minSize !== null ) { $options['minSize'] = $this->minSize; $options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [ + 'attribute' => $label, 'limit' => $this->minSize ], Yii::$app->language); } - + if ( $this->maxSize !== null ) { $options['maxSize'] = $this->maxSize; $options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [ + 'attribute' => $label, 'limit' => $this->maxSize - ], Yii::$app->language); - } - + ], Yii::$app->language); + } + if ( $this->maxFiles !== null ) { $options['maxFiles'] = $this->maxFiles; $options['tooMany'] = Yii::$app->getI18n()->format($this->tooMany, [ + 'attribute' => $label, 'limit' => $this->maxFiles ], Yii::$app->language); } - - ValidationAsset::register($view); - - return 'yii.validation.file(value, messages, ' . json_encode($options) . ', attribute);'; + + return $options; } } diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index 0f84b4d..c59722f 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -134,7 +134,7 @@ class ImageValidator extends FileValidator return [$this->notImage, ['file' => $image->name]]; } - list($width, $height, $type) = $imageInfo; + list($width, $height) = $imageInfo; if ($width == 0 || $height == 0) { return [$this->notImage, ['file' => $image->name]]; @@ -158,4 +158,64 @@ class ImageValidator extends FileValidator return null; } + + /** + * @inheritdoc + */ + public function clientValidateAttribute($object, $attribute, $view) + { + ValidationAsset::register($view); + $options = $this->getClientOptions($object, $attribute); + return 'yii.validation.image(attribute, messages, ' . json_encode($options) . ', deferred);'; + } + + /** + * @inheritdoc + */ + protected function getClientOptions($object, $attribute) + { + $options = parent::getClientOptions($object, $attribute); + + $label = $object->getAttributeLabel($attribute); + + if ($this->notImage !== null) { + $options['notImage'] = Yii::$app->getI18n()->format($this->notImage, [ + 'attribute' => $label + ], Yii::$app->language); + } + + if ($this->minWidth !== null) { + $options['minWidth'] = $this->minWidth; + $options['underWidth'] = Yii::$app->getI18n()->format($this->underWidth, [ + 'attribute' => $label, + 'limit' => $this->minWidth + ], Yii::$app->language); + } + + if ($this->maxWidth !== null) { + $options['maxWidth'] = $this->maxWidth; + $options['overWidth'] = Yii::$app->getI18n()->format($this->overWidth, [ + 'attribute' => $label, + 'limit' => $this->maxWidth + ], Yii::$app->language); + } + + if ($this->minHeight !== null) { + $options['minHeight'] = $this->minHeight; + $options['underHeight'] = Yii::$app->getI18n()->format($this->underHeight, [ + 'attribute' => $label, + 'limit' => $this->maxHeight + ], Yii::$app->language); + } + + if ($this->maxHeight !== null) { + $options['maxHeight'] = $this->maxHeight; + $options['overHeight'] = Yii::$app->getI18n()->format($this->overHeight, [ + 'attribute' => $label, + 'limit' => $this->maxHeight + ], Yii::$app->language); + } + + return $options; + } } diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php index 0891690..75989bb 100644 --- a/tests/unit/data/base/Singer.php +++ b/tests/unit/data/base/Singer.php @@ -10,6 +10,7 @@ class Singer extends Model { public $firstName; public $lastName; + public $test; public function rules() { @@ -17,6 +18,7 @@ class Singer extends Model [['lastName'], 'default', 'value' => 'Lennon'], [['lastName'], 'required'], [['underscore_style'], 'yii\captcha\CaptchaValidator'], + [['test'], 'required', 'when' => function($model) { return $model->firstName === 'cebe'; }], ]; } } diff --git a/tests/unit/extensions/gii/GeneratorsTest.php b/tests/unit/extensions/gii/GeneratorsTest.php index f8cb16c..c93b1dc 100644 --- a/tests/unit/extensions/gii/GeneratorsTest.php +++ b/tests/unit/extensions/gii/GeneratorsTest.php @@ -1,6 +1,7 @@ <?php namespace yiiunit\extensions\gii; +use yii\gii\CodeFile; use yii\gii\generators\controller\Generator as ControllerGenerator; use yii\gii\generators\crud\Generator as CRUDGenerator; use yii\gii\generators\extension\Generator as ExtensionGenerator; @@ -53,7 +54,11 @@ class GeneratorsTest extends GiiTestCase $generator->modelClass = 'Profile'; if ($generator->validate()) { - $generator->generate(); + $files = $generator->generate(); + $modelCode = $files[0]->content; + + $this->assertTrue(strpos($modelCode, "'id' => 'ID'") !== false, "ID label should be there:\n" . $modelCode); + $this->assertTrue(strpos($modelCode, "'description' => 'Description',") !== false, "Description label should be there:\n" . $modelCode); } else { print_r($generator->getErrors()); } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index f64afd7..6a2cd2a 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -216,7 +216,7 @@ class ModelTest extends TestCase public function testDefaultScenarios() { $singer = new Singer(); - $this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); + $this->assertEquals(['default' => ['lastName', 'underscore_style', 'test']], $singer->scenarios()); $scenarios = [ 'default' => ['id', 'name', 'description'], @@ -238,6 +238,13 @@ class ModelTest extends TestCase $singer = new Singer(); $this->assertFalse($singer->isAttributeRequired('firstName')); $this->assertTrue($singer->isAttributeRequired('lastName')); + + // attribute is not marked as required when a conditional validation is applied using `$when`. + // the condition should not be applied because this info may be retrieved before model is loaded with data + $singer->firstName = 'qiang'; + $this->assertFalse($singer->isAttributeRequired('test')); + $singer->firstName = 'cebe'; + $this->assertFalse($singer->isAttributeRequired('test')); } public function testCreateValidators() diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index d05e0b4..af2f157 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -152,10 +152,93 @@ class QueryBuilderTest extends DatabaseTestCase [ ['or like', 'name', ['heyho', 'abc']], '"name" LIKE :qp0 OR "name" LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ], [ ['or not like', 'name', ['heyho', 'abc']], '"name" NOT LIKE :qp0 OR "name" NOT LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ], - // TODO add more conditions - // IN - // NOT - // ... + // not + [ ['not', 'name'], 'NOT (name)', [] ], + + // and + [ ['and', 'id=1', 'id=2'], '(id=1) AND (id=2)', [] ], + [ ['and', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) AND ((id=1) OR (id=2))', [] ], + + // or + [ ['or', 'id=1', 'id=2'], '(id=1) OR (id=2)', [] ], + [ ['or', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) OR ((id=1) OR (id=2))', [] ], + + + // between + [ ['between', 'id', 1, 10], '"id" BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10] ], + [ ['not between', 'id', 1, 10], '"id" NOT BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10] ], + + // in + [ ['in', 'id', [1, 2, 3]], '"id" IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3] ], + [ ['not in', 'id', [1, 2, 3]], '"id" NOT IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3] ], + + // TODO: exists and not exists + + // simple conditions + [ ['=', 'a', 'b'], '"a" = :qp0', [':qp0' => 'b'] ], + [ ['>', 'a', 1], '"a" > :qp0', [':qp0' => 1] ], + [ ['>=', 'a', 'b'], '"a" >= :qp0', [':qp0' => 'b'] ], + [ ['<', 'a', 2], '"a" < :qp0', [':qp0' => 2] ], + [ ['<=', 'a', 'b'], '"a" <= :qp0', [':qp0' => 'b'] ], + [ ['<>', 'a', 3], '"a" <> :qp0', [':qp0' => 3] ], + [ ['!=', 'a', 'b'], '"a" != :qp0', [':qp0' => 'b'] ], + ]; + + // adjust dbms specific escaping + foreach($conditions as $i => $condition) { + switch ($this->driverName) { + case 'mssql': + case 'mysql': + case 'sqlite': + $conditions[$i][1] = str_replace('"', '`', $condition[1]); + break; + } + + } + return $conditions; + } + + public function filterConditionProvider() + { + $conditions = [ + // like + [ ['like', 'name', []], '', [] ], + [ ['not like', 'name', []], '', [] ], + [ ['or like', 'name', []], '', [] ], + [ ['or not like', 'name', []], '', [] ], + + // not + [ ['not', ''], '', [] ], + + // and + [ ['and', '', ''], '', [] ], + [ ['and', '', 'id=2'], '(id=2)', [] ], + [ ['and', 'id=1', ''], '(id=1)', [] ], + [ ['and', 'type=1', ['or', '', 'id=2']], '(type=1) AND ((id=2))', [] ], + + // or + [ ['or', 'id=1', ''], '(id=1)', [] ], + [ ['or', 'type=1', ['or', '', 'id=2']], '(type=1) OR ((id=2))', [] ], + + + // between + [ ['between', 'id', 1, null], '', [] ], + [ ['not between', 'id', null, 10], '', [] ], + + // in + [ ['in', 'id', []], '', [] ], + [ ['not in', 'id', []], '', [] ], + + // TODO: exists and not exists + + // simple conditions + [ ['=', 'a', ''], '', [] ], + [ ['>', 'a', ''], '', [] ], + [ ['>=', 'a', ''], '', [] ], + [ ['<', 'a', ''], '', [] ], + [ ['<=', 'a', ''], '', [] ], + [ ['<>', 'a', ''], '', [] ], + [ ['!=', 'a', ''], '', [] ], ]; // adjust dbms specific escaping @@ -183,6 +266,17 @@ class QueryBuilderTest extends DatabaseTestCase $this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql); } + /** + * @dataProvider filterConditionProvider + */ + public function testBuildFilterCondition($condition, $expected, $expectedParams) + { + $query = (new Query())->filterWhere($condition); + list($sql, $params) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedParams, $params); + $this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql); + } + public function testAddDropPrimaryKey() { $tableName = 'constraints'; diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php index d22c8c4..6bd9cd9 100644 --- a/tests/unit/framework/rbac/PhpManagerTest.php +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -75,13 +75,13 @@ class PhpManagerTest extends ManagerTestCase public function testSaveLoad() { - static::$filemtime = time(); $this->prepareData(); $items = $this->auth->items; $children = $this->auth->children; $assignments = $this->auth->assignments; $rules = $this->auth->rules; + static::$filemtime = time(); $this->auth->save(); $this->auth = $this->createManager(); diff --git a/tests/unit/framework/widgets/ActiveFieldTest.php b/tests/unit/framework/widgets/ActiveFieldTest.php index 1330342..f4b3e2a 100644 --- a/tests/unit/framework/widgets/ActiveFieldTest.php +++ b/tests/unit/framework/widgets/ActiveFieldTest.php @@ -266,7 +266,7 @@ EOD; $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator'); $this->activeField->enableClientValidation = true; $actualValue = $this->activeField->getClientOptions(); - $expectedJsExpression = "function (attribute, value, messages) {return true;}"; + $expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}"; $expectedValidateOnChange = true; $expectedValidateOnType = false; $expectedValidationDelay = 200; @@ -286,7 +286,7 @@ EOD; $this->activeField->enableAjaxValidation = true; $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator'); $actualValue = $this->activeField->getClientOptions(); - $expectedJsExpression = "function (attribute, value, messages) {return true;}"; + $expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}"; $expectedValidateOnChange = true; $expectedValidateOnType = false; $expectedValidationDelay = 200; @@ -313,7 +313,7 @@ EOD; } $actualValue = $this->activeField->getClientOptions(); - $expectedJsExpression = "function (attribute, value, messages) {if (function (attribute, value) " + $expectedJsExpression = "function (attribute, value, messages, deferred) {if (function (attribute, value) " . "{ return 'yii2' == 'yii2'; }(attribute, value)) { return true; }}"; $expectedValidateOnChange = true;