Commit d5bd6816 by RichWeber

Merge branch 'master' of github.com:yiisoft/yii2

parents 9fa9dee3 f0fefbb6
...@@ -13,10 +13,6 @@ return [ ...@@ -13,10 +13,6 @@ return [
'bootstrap' => ['log'], 'bootstrap' => ['log'],
'modules' => [], 'modules' => [],
'components' => [ 'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'user' => [ 'user' => [
'identityClass' => 'common\models\User', 'identityClass' => 'common\models\User',
'enableAutoLogin' => true, 'enableAutoLogin' => true,
......
...@@ -32,8 +32,7 @@ ...@@ -32,8 +32,7 @@
}, },
"scripts": { "scripts": {
"post-create-project-cmd": [ "post-create-project-cmd": [
"yii\\composer\\Installer::setPermission", "yii\\composer\\Installer::setPermission"
"yii\\composer\\Installer::generateCookieValidationKey"
] ]
}, },
"config": { "config": {
...@@ -46,10 +45,6 @@ ...@@ -46,10 +45,6 @@
"frontend/runtime", "frontend/runtime",
"frontend/web/assets" "frontend/web/assets"
],
"config": [
"frontend/config/main.php",
"backend/config/main.php"
] ]
} }
} }
<?php <?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) { if (!YII_ENV_TEST) {
// configuration adjustments for 'dev' environment // configuration adjustments for 'dev' environment
......
<?php <?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) { if (!YII_ENV_TEST) {
// configuration adjustments for 'dev' environment // configuration adjustments for 'dev' environment
......
...@@ -9,9 +9,15 @@ ...@@ -9,9 +9,15 @@
* return [ * return [
* 'environment name' => [ * 'environment name' => [
* 'path' => 'directory storing the local files', * 'path' => 'directory storing the local files',
* 'writable' => [ * 'setWritable' => [
* // list of directories that should be set writable * // 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 @@ ...@@ -19,26 +25,34 @@
return [ return [
'Development' => [ 'Development' => [
'path' => 'dev', 'path' => 'dev',
'writable' => [ 'setWritable' => [
'backend/runtime', 'backend/runtime',
'backend/web/assets', 'backend/web/assets',
'frontend/runtime', 'frontend/runtime',
'frontend/web/assets', 'frontend/web/assets',
], ],
'executable' => [ 'setExecutable' => [
'yii', 'yii',
], ],
'setCookieValidationKey' => [
'backend/config/main-local.php',
'frontend/config/main-local.php',
],
], ],
'Production' => [ 'Production' => [
'path' => 'prod', 'path' => 'prod',
'writable' => [ 'setWritable' => [
'backend/runtime', 'backend/runtime',
'backend/web/assets', 'backend/web/assets',
'frontend/runtime', 'frontend/runtime',
'frontend/web/assets', 'frontend/web/assets',
], ],
'executable' => [ 'setExecutable' => [
'yii', 'yii',
], ],
'setCookieValidationKey' => [
'backend/config/main-local.php',
'frontend/config/main-local.php',
],
], ],
]; ];
<?php <?php
return [ return [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
]; ];
<?php <?php
return [ return [
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
],
]; ];
...@@ -12,10 +12,6 @@ return [ ...@@ -12,10 +12,6 @@ return [
'bootstrap' => ['log'], 'bootstrap' => ['log'],
'controllerNamespace' => 'frontend\controllers', 'controllerNamespace' => 'frontend\controllers',
'components' => [ 'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '',
],
'user' => [ 'user' => [
'identityClass' => 'common\models\User', 'identityClass' => 'common\models\User',
'enableAutoLogin' => true, 'enableAutoLogin' => true,
......
...@@ -14,6 +14,10 @@ ...@@ -14,6 +14,10 @@
* @license http://www.yiiframework.com/license/ * @license http://www.yiiframework.com/license/
*/ */
if (!extension_loaded('mcrypt')) {
die('The mcrypt PHP extension is required by Yii2.');
}
$params = getParams(); $params = getParams();
$root = str_replace('\\', '/', __DIR__); $root = str_replace('\\', '/', __DIR__);
$envs = require("$root/environments/index.php"); $envs = require("$root/environments/index.php");
...@@ -23,147 +27,169 @@ echo "Yii Application Initialization Tool v1.0\n\n"; ...@@ -23,147 +27,169 @@ echo "Yii Application Initialization Tool v1.0\n\n";
$envName = null; $envName = null;
if (empty($params['env']) || $params['env'] === '1') { if (empty($params['env']) || $params['env'] === '1') {
echo "Which environment do you want the application to be initialized in?\n\n"; echo "Which environment do you want the application to be initialized in?\n\n";
foreach ($envNames as $i => $name) { foreach ($envNames as $i => $name) {
echo " [$i] $name\n"; echo " [$i] $name\n";
} }
echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] ';
$answer = trim(fgets(STDIN)); $answer = trim(fgets(STDIN));
if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) {
echo "\n Quit initialization.\n"; echo "\n Quit initialization.\n";
exit(0); exit(0);
} }
if (isset($envNames[$answer])) { if (isset($envNames[$answer])) {
$envName = $envNames[$answer]; $envName = $envNames[$answer];
} }
} else { } else {
$envName = $params['env']; $envName = $params['env'];
} }
if (!in_array($envName, $envNames)) { if (!in_array($envName, $envNames)) {
$envsList = implode(', ', $envNames); $envsList = implode(', ', $envNames);
echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n"; echo "\n $envName is not a valid environment. Try one of the following: $envsList. \n";
exit(2); exit(2);
} }
$env = $envs[$envName]; $env = $envs[$envName];
if (empty($params['env'])) { if (empty($params['env'])) {
echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] ";
$answer = trim(fgets(STDIN)); $answer = trim(fgets(STDIN));
if (strncasecmp($answer, 'y', 1)) { if (strncasecmp($answer, 'y', 1)) {
echo "\n Quit initialization.\n"; echo "\n Quit initialization.\n";
exit(0); exit(0);
} }
} }
echo "\n Start initialization ...\n\n"; echo "\n Start initialization ...\n\n";
$files = getFileList("$root/environments/{$env['path']}"); $files = getFileList("$root/environments/{$env['path']}");
$all = false; $all = false;
foreach ($files as $file) { foreach ($files as $file) {
if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) { if (!copyFile($root, "environments/{$env['path']}/$file", $file, $all, $params)) {
break; break;
} }
}
if (isset($env['writable'])) {
foreach ($env['writable'] as $writable) {
echo " chmod 0777 $writable\n";
@chmod("$root/$writable", 0777);
}
} }
if (isset($env['executable'])) { $callbacks = ['setCookieValidationKey', 'setWritable', 'setExecutable'];
foreach ($env['executable'] as $executable) { foreach ($callbacks as $callback) {
echo " chmod 0755 $executable\n"; if (!empty($env[$callback])) {
@chmod("$root/$executable", 0755); $callback($root, $env[$callback]);
} }
} }
echo "\n ... initialization completed.\n\n"; echo "\n ... initialization completed.\n\n";
function getFileList($root, $basePath = '') function getFileList($root, $basePath = '')
{ {
$files = []; $files = [];
$handle = opendir($root); $handle = opendir($root);
while (($path = readdir($handle)) !== false) { while (($path = readdir($handle)) !== false) {
if ($path === '.svn' || $path === '.' || $path === '..') { if ($path === '.svn' || $path === '.' || $path === '..') {
continue; continue;
} }
$fullPath = "$root/$path"; $fullPath = "$root/$path";
$relativePath = $basePath === '' ? $path : "$basePath/$path"; $relativePath = $basePath === '' ? $path : "$basePath/$path";
if (is_dir($fullPath)) { if (is_dir($fullPath)) {
$files = array_merge($files, getFileList($fullPath, $relativePath)); $files = array_merge($files, getFileList($fullPath, $relativePath));
} else { } else {
$files[] = $relativePath; $files[] = $relativePath;
} }
} }
closedir($handle); closedir($handle);
return $files; return $files;
} }
function copyFile($root, $source, $target, &$all, $params) function copyFile($root, $source, $target, &$all, $params)
{ {
if (!is_file($root . '/' . $source)) { if (!is_file($root . '/' . $source)) {
echo " skip $target ($source not exist)\n"; echo " skip $target ($source not exist)\n";
return true; return true;
} }
if (is_file($root . '/' . $target)) { if (is_file($root . '/' . $target)) {
if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) { if (file_get_contents($root . '/' . $source) === file_get_contents($root . '/' . $target)) {
echo " unchanged $target\n"; echo " unchanged $target\n";
return true; return true;
} }
if ($all) { if ($all) {
echo " overwrite $target\n"; echo " overwrite $target\n";
} else { } else {
echo " exist $target\n"; echo " exist $target\n";
echo " ...overwrite? [Yes|No|All|Quit] "; echo " ...overwrite? [Yes|No|All|Quit] ";
$answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN)); $answer = !empty($params['overwrite']) ? $params['overwrite'] : trim(fgets(STDIN));
if (!strncasecmp($answer, 'q', 1)) { if (!strncasecmp($answer, 'q', 1)) {
return false; return false;
} else { } else {
if (!strncasecmp($answer, 'y', 1)) { if (!strncasecmp($answer, 'y', 1)) {
echo " overwrite $target\n"; echo " overwrite $target\n";
} else { } else {
if (!strncasecmp($answer, 'a', 1)) { if (!strncasecmp($answer, 'a', 1)) {
echo " overwrite $target\n"; echo " overwrite $target\n";
$all = true; $all = true;
} else { } else {
echo " skip $target\n"; echo " skip $target\n";
return true; return true;
} }
} }
} }
} }
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true; return true;
} }
echo " generate $target\n"; echo " generate $target\n";
@mkdir(dirname($root . '/' . $target), 0777, true); @mkdir(dirname($root . '/' . $target), 0777, true);
file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source)); file_put_contents($root . '/' . $target, file_get_contents($root . '/' . $source));
return true; return true;
} }
function getParams() function getParams()
{ {
$rawParams = []; $rawParams = [];
if (isset($_SERVER['argv'])) { if (isset($_SERVER['argv'])) {
$rawParams = $_SERVER['argv']; $rawParams = $_SERVER['argv'];
array_shift($rawParams); array_shift($rawParams);
} }
$params = []; $params = [];
foreach ($rawParams as $param) { foreach ($rawParams as $param) {
if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) { if (preg_match('/^--(\w+)(=(.*))?$/', $param, $matches)) {
$name = $matches[1]; $name = $matches[1];
$params[$name] = isset($matches[3]) ? $matches[3] : true; $params[$name] = isset($matches[3]) ? $matches[3] : true;
} else { } else {
$params[] = $param; $params[] = $param;
} }
} }
return $params; 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);
}
} }
...@@ -97,7 +97,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration ...@@ -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. 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 The column types used in this example are abstract types that will be replaced
......
...@@ -249,6 +249,19 @@ Operator can be one of the following: ...@@ -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. - `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()`: If you are building parts of condition dynamically it's very convenient to use `andWhere()` and `orWhere()`:
```php ```php
...@@ -305,8 +318,6 @@ $query->orderBy([ ...@@ -305,8 +318,6 @@ $query->orderBy([
Here we are ordering by `id` ascending and then by `name` descending. Here we are ordering by `id` ascending and then by `name` descending.
```
### `GROUP BY` and `HAVING` ### `GROUP BY` and `HAVING`
In order to add `GROUP BY` to generated SQL you can use the following: In order to add `GROUP BY` to generated SQL you can use the following:
......
...@@ -56,7 +56,7 @@ return $this->render('renderer.twig', ['username' => 'Alex']); ...@@ -56,7 +56,7 @@ return $this->render('renderer.twig', ['username' => 'Alex']);
### Template syntax ### Template syntax
The best resource to learn Twig basics is its official documentation you can find at 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. described below.
#### Method and function calls #### Method and function calls
...@@ -271,7 +271,13 @@ or `$this->renderPartial()` controller calls: ...@@ -271,7 +271,13 @@ or `$this->renderPartial()` controller calls:
return $this->render('renderer.tpl', ['username' => 'Alex']); 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: Yii adds the following construct to the standard Smarty syntax:
...@@ -281,7 +287,7 @@ 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. 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: Within Smarty templates, you can also make use of these variables:
......
...@@ -197,7 +197,7 @@ class Generator extends \yii\gii\Generator ...@@ -197,7 +197,7 @@ class Generator extends \yii\gii\Generator
$labels[$column->name] = 'ID'; $labels[$column->name] = 'ID';
} else { } else {
$label = Inflector::camel2words($column->name); $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'; $label = substr($label, 0, -3) . ' ID';
} }
$labels[$column->name] = $label; $labels[$column->name] = $label;
...@@ -508,16 +508,16 @@ class Generator extends \yii\gii\Generator ...@@ -508,16 +508,16 @@ class Generator extends \yii\gii\Generator
} }
} }
private $_tableNames; protected $tableNames;
private $_classNames; protected $classNames;
/** /**
* @return array the table names that match the pattern specified by [[tableName]]. * @return array the table names that match the pattern specified by [[tableName]].
*/ */
protected function getTableNames() protected function getTableNames()
{ {
if ($this->_tableNames !== null) { if ($this->tableNames !== null) {
return $this->_tableNames; return $this->tableNames;
} }
$db = $this->getDbConnection(); $db = $this->getDbConnection();
if ($db === null) { if ($db === null) {
...@@ -540,10 +540,10 @@ class Generator extends \yii\gii\Generator ...@@ -540,10 +540,10 @@ class Generator extends \yii\gii\Generator
} }
} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
$tableNames[] = $this->tableName; $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 ...@@ -574,8 +574,8 @@ class Generator extends \yii\gii\Generator
*/ */
protected function generateClassName($tableName) protected function generateClassName($tableName)
{ {
if (isset($this->_classNames[$tableName])) { if (isset($this->classNames[$tableName])) {
return $this->_classNames[$tableName]; return $this->classNames[$tableName];
} }
if (($pos = strrpos($tableName, '.')) !== false) { if (($pos = strrpos($tableName, '.')) !== false) {
...@@ -601,7 +601,7 @@ class Generator extends \yii\gii\Generator ...@@ -601,7 +601,7 @@ class Generator extends \yii\gii\Generator
} }
} }
return $this->_classNames[$tableName] = Inflector::id2camel($className, '_'); return $this->classNames[$tableName] = Inflector::id2camel($className, '_');
} }
/** /**
......
...@@ -617,12 +617,11 @@ class QueryBuilder extends Object ...@@ -617,12 +617,11 @@ class QueryBuilder extends Object
$operator = strtoupper($condition[0]); $operator = strtoupper($condition[0]);
if (isset($builders[$operator])) { if (isset($builders[$operator])) {
$method = $builders[$operator]; $method = $builders[$operator];
array_shift($condition);
return $this->$method($indexes, $operator, $condition, $params);
} else { } 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', ... } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($indexes, $condition, $params); return $this->buildHashCondition($indexes, $condition, $params);
...@@ -986,4 +985,29 @@ class QueryBuilder extends Object ...@@ -986,4 +985,29 @@ class QueryBuilder extends Object
return $phName; 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";
}
} }
...@@ -144,16 +144,30 @@ class ViewRenderer extends BaseViewRenderer ...@@ -144,16 +144,30 @@ class ViewRenderer extends BaseViewRenderer
{ {
$this->twig->addGlobal('this', $view); $this->twig->addGlobal('this', $view);
$loader = new \Twig_Loader_Filesystem(dirname($file)); $loader = new \Twig_Loader_Filesystem(dirname($file));
$this->addAliases($loader, Yii::$aliases);
foreach (Yii::$aliases as $alias => $path) {
$loader->addPath($path, substr($alias, 1));
}
$this->twig->setLoader($loader); $this->twig->setLoader($loader);
return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params); 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 * Adds global objects or static classes
* @param array $globals @see self::$globals * @param array $globals @see self::$globals
*/ */
......
...@@ -90,6 +90,7 @@ Yii Framework 2 Change Log ...@@ -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 #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 #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 #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 #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 #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) - Enh #2837: Error page now shows arguments in stack trace method calls (samdark)
...@@ -167,6 +168,7 @@ Yii Framework 2 Change Log ...@@ -167,6 +168,7 @@ Yii Framework 2 Change Log
- Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma) - Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma)
- Enh #4485: Added support for deferred validation in `ActiveForm` (Alex-Code) - Enh #4485: Added support for deferred validation in `ActiveForm` (Alex-Code)
- Enh #4520: Added sasl support to `yii\caching\MemCache` (xjflyttp) - 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: 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: 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) - Enh: Added `yii\web\UrlManager::addRules()` to simplify adding new URL rules (qiangxue)
......
...@@ -68,55 +68,67 @@ yii.validation = (function ($) { ...@@ -68,55 +68,67 @@ yii.validation = (function ($) {
pub.addMessage(messages, options.notEqual, value); pub.addMessage(messages, options.notEqual, value);
} }
}, },
file: function (value, messages, options, attribute) { file: function (attribute, messages, options) {
var files = $(attribute.input).get(0).files, var files = getUploadedFiles(attribute, messages, options);
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);
}
$.each(files, function (i, file) { $.each(files, function (i, file) {
if (options.extensions && options.extensions.length > 0) { validateFile(file, messages, options);
index = file.name.lastIndexOf('.'); });
},
if (!~index) {
ext = ''; image: function (attribute, messages, options, deferred) {
} else { var files = getUploadedFiles(attribute, messages, options);
ext = file.name.substr(index + 1, file.name.length).toLowerCase();
} $.each(files, function (i, file) {
validateFile(file, messages, options);
if (!~options.extensions.indexOf(ext)) { // Skip image validation if FileReader API is not available
messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); if (typeof FileReader === "undefined") {
} return;
} }
if (options.mimeTypes && options.mimeTypes.length > 0) { var def = $.Deferred(),
if (!~options.mimeTypes.indexOf(file.type)) { fr = new FileReader(),
messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name)); img = new Image();
img.onload = function () {
if (options.minWidth && this.width < options.minWidth) {
messages.push(options.underWidth.replace(/\{file\}/g, file.name));
} }
}
if (options.maxWidth && this.width > options.maxWidth) {
if (options.maxSize && options.maxSize < file.size) { messages.push(options.overWidth.replace(/\{file\}/g, file.name));
messages.push(options.tooBig.replace(/\{file\}/g, file.name)); }
}
if (options.minHeight && this.height < options.minHeight) {
if (options.maxSize && options.minSize > file.size) { messages.push(options.underHeight.replace(/\{file\}/g, file.name));
messages.push(options.tooSmall.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) { number: function (value, messages, options) {
...@@ -288,5 +300,60 @@ yii.validation = (function ($) { ...@@ -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; return pub;
})(jQuery); })(jQuery);
...@@ -385,7 +385,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -385,7 +385,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
if ($this->_validators === null) { if ($this->_validators === null) {
$this->_validators = $this->createValidators(); $this->_validators = $this->createValidators();
} }
return $this->_validators; return $this->_validators;
} }
...@@ -404,7 +403,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -404,7 +403,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
$validators[] = $validator; $validators[] = $validator;
} }
} }
return $validators; return $validators;
} }
...@@ -427,7 +425,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -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.'); throw new InvalidConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
} }
} }
return $validators; return $validators;
} }
...@@ -436,6 +433,12 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -436,6 +433,12 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
* This is determined by checking if the attribute is associated with a * This is determined by checking if the attribute is associated with a
* [[\yii\validators\RequiredValidator|required]] validation rule in the * [[\yii\validators\RequiredValidator|required]] validation rule in the
* current [[scenario]]. * 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 * @param string $attribute attribute name
* @return boolean whether the attribute is required * @return boolean whether the attribute is required
*/ */
...@@ -446,7 +449,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -446,7 +449,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
return true; return true;
} }
} }
return false; return false;
} }
...@@ -482,7 +484,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab ...@@ -482,7 +484,6 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab
public function getAttributeLabel($attribute) public function getAttributeLabel($attribute)
{ {
$labels = $this->attributeLabels(); $labels = $this->attributeLabels();
return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute); return isset($labels[$attribute]) ? $labels[$attribute] : $this->generateAttributeLabel($attribute);
} }
......
...@@ -93,7 +93,10 @@ class AttributeBehavior extends Behavior ...@@ -93,7 +93,10 @@ class AttributeBehavior extends Behavior
$attributes = (array) $this->attributes[$event->name]; $attributes = (array) $this->attributes[$event->name];
$value = $this->getValue($event); $value = $this->getValue($event);
foreach ($attributes as $attribute) { 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;
}
} }
} }
} }
......
...@@ -54,10 +54,12 @@ class BlameableBehavior extends AttributeBehavior ...@@ -54,10 +54,12 @@ class BlameableBehavior extends AttributeBehavior
{ {
/** /**
* @var string the attribute that will receive current user ID value * @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'; public $createdByAttribute = 'created_by';
/** /**
* @var string the attribute that will receive current user ID value * @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'; public $updatedByAttribute = 'updated_by';
/** /**
......
...@@ -64,10 +64,12 @@ class TimestampBehavior extends AttributeBehavior ...@@ -64,10 +64,12 @@ class TimestampBehavior extends AttributeBehavior
{ {
/** /**
* @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 creation time.
*/ */
public $createdAtAttribute = 'created_at'; 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'; public $updatedAtAttribute = 'updated_at';
/** /**
......
...@@ -96,6 +96,9 @@ class MessageController extends Controller ...@@ -96,6 +96,9 @@ class MessageController extends Controller
if (!is_dir($config['sourcePath'])) { if (!is_dir($config['sourcePath'])) {
throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); 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 (in_array($config['format'], ['php', 'po'])) {
if (!isset($config['messagePath'])) { if (!isset($config['messagePath'])) {
throw new Exception('The configuration file must specify "messagePath".'); throw new Exception('The configuration file must specify "messagePath".');
...@@ -106,9 +109,6 @@ class MessageController extends Controller ...@@ -106,9 +109,6 @@ class MessageController extends Controller
if (empty($config['languages'])) { if (empty($config['languages'])) {
throw new Exception("Languages cannot be empty."); 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); $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
......
...@@ -868,7 +868,6 @@ class QueryBuilder extends \yii\base\Object ...@@ -868,7 +868,6 @@ class QueryBuilder extends \yii\base\Object
* on how to specify a condition. * on how to specify a condition.
* @param array $params the binding parameters to be populated * @param array $params the binding parameters to be populated
* @return string the generated SQL expression * @return string the generated SQL expression
* @throws InvalidParamException if the condition is in bad format
*/ */
public function buildCondition($condition, &$params) public function buildCondition($condition, &$params)
{ {
...@@ -882,11 +881,11 @@ class QueryBuilder extends \yii\base\Object ...@@ -882,11 +881,11 @@ class QueryBuilder extends \yii\base\Object
$operator = strtoupper($condition[0]); $operator = strtoupper($condition[0]);
if (isset($this->conditionBuilders[$operator])) { if (isset($this->conditionBuilders[$operator])) {
$method = $this->conditionBuilders[$operator]; $method = $this->conditionBuilders[$operator];
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else { } 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', ... } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params); return $this->buildHashCondition($condition, $params);
} }
...@@ -1194,4 +1193,29 @@ class QueryBuilder extends \yii\base\Object ...@@ -1194,4 +1193,29 @@ class QueryBuilder extends \yii\base\Object
throw new InvalidParamException('Subquery for EXISTS operator must be a Query 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";
}
} }
...@@ -253,20 +253,6 @@ trait QueryTrait ...@@ -253,20 +253,6 @@ trait QueryTrait
return []; return [];
} }
break; 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 'BETWEEN':
case 'NOT BETWEEN': case 'NOT BETWEEN':
if (array_key_exists(1, $condition) && array_key_exists(2, $condition)) { if (array_key_exists(1, $condition) && array_key_exists(2, $condition)) {
...@@ -276,7 +262,9 @@ trait QueryTrait ...@@ -276,7 +262,9 @@ trait QueryTrait
} }
break; break;
default: default:
throw new NotSupportedException("Operator not supported: $operator"); if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
return [];
}
} }
array_unshift($condition, $operator); array_unshift($condition, $operator);
......
...@@ -122,7 +122,7 @@ class FileValidator extends Validator ...@@ -122,7 +122,7 @@ class FileValidator extends Validator
* - {mimeTypes}: the value of [[mimeTypes]] * - {mimeTypes}: the value of [[mimeTypes]]
*/ */
public $wrongMimeType; public $wrongMimeType;
/** /**
* @inheritdoc * @inheritdoc
...@@ -150,12 +150,16 @@ class FileValidator extends Validator ...@@ -150,12 +150,16 @@ class FileValidator extends Validator
} }
if (!is_array($this->extensions)) { if (!is_array($this->extensions)) {
$this->extensions = preg_split('/[\s,]+/', strtolower($this->extensions), -1, PREG_SPLIT_NO_EMPTY); $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) { if ($this->wrongMimeType === null) {
$this->wrongMimeType = Yii::t('yii', 'Only files with these MIME types are allowed: {mimeTypes}.'); $this->wrongMimeType = Yii::t('yii', 'Only files with these MIME types are allowed: {mimeTypes}.');
} }
if (!is_array($this->mimeTypes)) { if (!is_array($this->mimeTypes)) {
$this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY); $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 ...@@ -330,58 +334,77 @@ class FileValidator extends Validator
/** /**
* @inheritdoc * @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); $label = $object->getAttributeLabel($attribute);
if ( $this->message !== null ){ if ( $this->message !== null ){
$options['message'] = Yii::$app->getI18n()->format($this->message, [ $options['message'] = Yii::$app->getI18n()->format($this->message, [
'attribute' => $label, 'attribute' => $label,
], Yii::$app->language); ], Yii::$app->language);
} }
$options['skipOnEmpty'] = $this->skipOnEmpty; $options['skipOnEmpty'] = $this->skipOnEmpty;
if ( !$this->skipOnEmpty ) { if ( !$this->skipOnEmpty ) {
$options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [], Yii::$app->language); $options['uploadRequired'] = Yii::$app->getI18n()->format($this->uploadRequired, [
'attribute' => $label,
], Yii::$app->language);
} }
if ( $this->mimeTypes !== null ) { if ( $this->mimeTypes !== null ) {
$options['mimeTypes'] = $this->mimeTypes; $options['mimeTypes'] = $this->mimeTypes;
$options['wrongMimeType'] = Yii::$app->getI18n()->format($this->wrongMimeType, [ $options['wrongMimeType'] = Yii::$app->getI18n()->format($this->wrongMimeType, [
'attribute' => $label,
'mimeTypes' => join(', ', $this->mimeTypes) 'mimeTypes' => join(', ', $this->mimeTypes)
], Yii::$app->language); ], Yii::$app->language);
} }
if ( $this->extensions !== null ) { if ( $this->extensions !== null ) {
$options['extensions'] = $this->extensions; $options['extensions'] = $this->extensions;
$options['wrongExtension'] = Yii::$app->getI18n()->format($this->wrongExtension, [ $options['wrongExtension'] = Yii::$app->getI18n()->format($this->wrongExtension, [
'attribute' => $label,
'extensions' => join(', ', $this->extensions) 'extensions' => join(', ', $this->extensions)
], Yii::$app->language); ], Yii::$app->language);
} }
if ( $this->minSize !== null ) { if ( $this->minSize !== null ) {
$options['minSize'] = $this->minSize; $options['minSize'] = $this->minSize;
$options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [ $options['tooSmall'] = Yii::$app->getI18n()->format($this->tooSmall, [
'attribute' => $label,
'limit' => $this->minSize 'limit' => $this->minSize
], Yii::$app->language); ], Yii::$app->language);
} }
if ( $this->maxSize !== null ) { if ( $this->maxSize !== null ) {
$options['maxSize'] = $this->maxSize; $options['maxSize'] = $this->maxSize;
$options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [ $options['tooBig'] = Yii::$app->getI18n()->format($this->tooBig, [
'attribute' => $label,
'limit' => $this->maxSize 'limit' => $this->maxSize
], Yii::$app->language); ], Yii::$app->language);
} }
if ( $this->maxFiles !== null ) { if ( $this->maxFiles !== null ) {
$options['maxFiles'] = $this->maxFiles; $options['maxFiles'] = $this->maxFiles;
$options['tooMany'] = Yii::$app->getI18n()->format($this->tooMany, [ $options['tooMany'] = Yii::$app->getI18n()->format($this->tooMany, [
'attribute' => $label,
'limit' => $this->maxFiles 'limit' => $this->maxFiles
], Yii::$app->language); ], Yii::$app->language);
} }
ValidationAsset::register($view); return $options;
return 'yii.validation.file(value, messages, ' . json_encode($options) . ', attribute);';
} }
} }
...@@ -134,7 +134,7 @@ class ImageValidator extends FileValidator ...@@ -134,7 +134,7 @@ class ImageValidator extends FileValidator
return [$this->notImage, ['file' => $image->name]]; return [$this->notImage, ['file' => $image->name]];
} }
list($width, $height, $type) = $imageInfo; list($width, $height) = $imageInfo;
if ($width == 0 || $height == 0) { if ($width == 0 || $height == 0) {
return [$this->notImage, ['file' => $image->name]]; return [$this->notImage, ['file' => $image->name]];
...@@ -158,4 +158,64 @@ class ImageValidator extends FileValidator ...@@ -158,4 +158,64 @@ class ImageValidator extends FileValidator
return null; 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;
}
} }
...@@ -10,6 +10,7 @@ class Singer extends Model ...@@ -10,6 +10,7 @@ class Singer extends Model
{ {
public $firstName; public $firstName;
public $lastName; public $lastName;
public $test;
public function rules() public function rules()
{ {
...@@ -17,6 +18,7 @@ class Singer extends Model ...@@ -17,6 +18,7 @@ class Singer extends Model
[['lastName'], 'default', 'value' => 'Lennon'], [['lastName'], 'default', 'value' => 'Lennon'],
[['lastName'], 'required'], [['lastName'], 'required'],
[['underscore_style'], 'yii\captcha\CaptchaValidator'], [['underscore_style'], 'yii\captcha\CaptchaValidator'],
[['test'], 'required', 'when' => function($model) { return $model->firstName === 'cebe'; }],
]; ];
} }
} }
<?php <?php
namespace yiiunit\extensions\gii; namespace yiiunit\extensions\gii;
use yii\gii\CodeFile;
use yii\gii\generators\controller\Generator as ControllerGenerator; use yii\gii\generators\controller\Generator as ControllerGenerator;
use yii\gii\generators\crud\Generator as CRUDGenerator; use yii\gii\generators\crud\Generator as CRUDGenerator;
use yii\gii\generators\extension\Generator as ExtensionGenerator; use yii\gii\generators\extension\Generator as ExtensionGenerator;
...@@ -53,7 +54,11 @@ class GeneratorsTest extends GiiTestCase ...@@ -53,7 +54,11 @@ class GeneratorsTest extends GiiTestCase
$generator->modelClass = 'Profile'; $generator->modelClass = 'Profile';
if ($generator->validate()) { 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 { } else {
print_r($generator->getErrors()); print_r($generator->getErrors());
} }
......
...@@ -216,7 +216,7 @@ class ModelTest extends TestCase ...@@ -216,7 +216,7 @@ class ModelTest extends TestCase
public function testDefaultScenarios() public function testDefaultScenarios()
{ {
$singer = new Singer(); $singer = new Singer();
$this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); $this->assertEquals(['default' => ['lastName', 'underscore_style', 'test']], $singer->scenarios());
$scenarios = [ $scenarios = [
'default' => ['id', 'name', 'description'], 'default' => ['id', 'name', 'description'],
...@@ -238,6 +238,13 @@ class ModelTest extends TestCase ...@@ -238,6 +238,13 @@ class ModelTest extends TestCase
$singer = new Singer(); $singer = new Singer();
$this->assertFalse($singer->isAttributeRequired('firstName')); $this->assertFalse($singer->isAttributeRequired('firstName'));
$this->assertTrue($singer->isAttributeRequired('lastName')); $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() public function testCreateValidators()
......
...@@ -152,10 +152,93 @@ class QueryBuilderTest extends DatabaseTestCase ...@@ -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 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%'] ], [ ['or not like', 'name', ['heyho', 'abc']], '"name" NOT LIKE :qp0 OR "name" NOT LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ],
// TODO add more conditions // not
// IN [ ['not', 'name'], 'NOT (name)', [] ],
// NOT
// ... // 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 // adjust dbms specific escaping
...@@ -183,6 +266,17 @@ class QueryBuilderTest extends DatabaseTestCase ...@@ -183,6 +266,17 @@ class QueryBuilderTest extends DatabaseTestCase
$this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql); $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() public function testAddDropPrimaryKey()
{ {
$tableName = 'constraints'; $tableName = 'constraints';
......
...@@ -75,13 +75,13 @@ class PhpManagerTest extends ManagerTestCase ...@@ -75,13 +75,13 @@ class PhpManagerTest extends ManagerTestCase
public function testSaveLoad() public function testSaveLoad()
{ {
static::$filemtime = time();
$this->prepareData(); $this->prepareData();
$items = $this->auth->items; $items = $this->auth->items;
$children = $this->auth->children; $children = $this->auth->children;
$assignments = $this->auth->assignments; $assignments = $this->auth->assignments;
$rules = $this->auth->rules; $rules = $this->auth->rules;
static::$filemtime = time();
$this->auth->save(); $this->auth->save();
$this->auth = $this->createManager(); $this->auth = $this->createManager();
......
...@@ -266,7 +266,7 @@ EOD; ...@@ -266,7 +266,7 @@ EOD;
$this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator'); $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
$this->activeField->enableClientValidation = true; $this->activeField->enableClientValidation = true;
$actualValue = $this->activeField->getClientOptions(); $actualValue = $this->activeField->getClientOptions();
$expectedJsExpression = "function (attribute, value, messages) {return true;}"; $expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}";
$expectedValidateOnChange = true; $expectedValidateOnChange = true;
$expectedValidateOnType = false; $expectedValidateOnType = false;
$expectedValidationDelay = 200; $expectedValidationDelay = 200;
...@@ -286,7 +286,7 @@ EOD; ...@@ -286,7 +286,7 @@ EOD;
$this->activeField->enableAjaxValidation = true; $this->activeField->enableAjaxValidation = true;
$this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator'); $this->activeField->model->addRule($this->attributeName, 'yiiunit\framework\widgets\TestValidator');
$actualValue = $this->activeField->getClientOptions(); $actualValue = $this->activeField->getClientOptions();
$expectedJsExpression = "function (attribute, value, messages) {return true;}"; $expectedJsExpression = "function (attribute, value, messages, deferred) {return true;}";
$expectedValidateOnChange = true; $expectedValidateOnChange = true;
$expectedValidateOnType = false; $expectedValidateOnType = false;
$expectedValidationDelay = 200; $expectedValidationDelay = 200;
...@@ -313,7 +313,7 @@ EOD; ...@@ -313,7 +313,7 @@ EOD;
} }
$actualValue = $this->activeField->getClientOptions(); $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; }}"; . "{ return 'yii2' == 'yii2'; }(attribute, value)) { return true; }}";
$expectedValidateOnChange = true; $expectedValidateOnChange = true;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment