diff --git a/docs/guide/apps-advanced.md b/docs/guide/apps-advanced.md index 890b1e9..5090b00 100644 --- a/docs/guide/apps-advanced.md +++ b/docs/guide/apps-advanced.md @@ -35,8 +35,8 @@ php /path/to/yii-application/init --- 2. Create a new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`. 3. In `common/config/params.php` set your database details in `components.db` values. - -4. Set document roots of your Web server: +4. Apply migrations with console command 'yii migrate'. +5. Set document roots of your Web server: - for frontend `/path/to/yii-application/frontend/web/` and using the URL `http://frontend/` - for backend `/path/to/yii-application/backend/web/` and using the URL `http://backend/` diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index caa6b61..06f0d1f 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -165,7 +165,7 @@ class Model extends Component implements IteratorAggregate, ArrayAccess * ] * ~~~ * - * By default, an active attribute that is considered safe and can be massively assigned. + * By default, an active attribute is considered safe and can be massively assigned. * If an attribute should NOT be massively assigned (thus considered unsafe), * please prefix the attribute with an exclamation character (e.g. '!rank'). * @@ -178,29 +178,49 @@ class Model extends Component implements IteratorAggregate, ArrayAccess */ public function scenarios() { - $scenarios = []; - $defaults = []; - /** @var $validator Validator */ + $scenarios = [self::DEFAULT_SCENARIO => []]; foreach ($this->getValidators() as $validator) { - if (empty($validator->on)) { - foreach ($validator->attributes as $attribute) { - $defaults[$attribute] = true; + foreach ($validator->on as $scenario) { + $scenarios[$scenario] = []; + } + foreach ($validator->except as $scenario) { + $scenarios[$scenario] = []; + } + } + $names = array_keys($scenarios); + + foreach ($this->getValidators() as $validator) { + if (empty($validator->on) && empty($validator->except)) { + foreach ($names as $name) { + foreach ($validator->attributes as $attribute) { + $scenarios[$name][$attribute] = true; + } + } + } elseif (empty($validator->on)) { + foreach ($names as $name) { + if (!in_array($name, $validator->except, true)) { + foreach ($validator->attributes as $attribute) { + $scenarios[$name][$attribute] = true; + } + } } } else { - foreach ($validator->on as $scenario) { + foreach ($validator->on as $name) { foreach ($validator->attributes as $attribute) { - $scenarios[$scenario][$attribute] = true; + $scenarios[$name][$attribute] = true; } } } } + foreach ($scenarios as $scenario => $attributes) { - foreach (array_keys($defaults) as $attribute) { - $attributes[$attribute] = true; + if (empty($attributes) && $scenario !== self::DEFAULT_SCENARIO) { + unset($scenarios[$scenario]); + } else { + $scenarios[$scenario] = array_keys($attributes); } - $scenarios[$scenario] = array_keys($attributes); } - $scenarios[self::DEFAULT_SCENARIO] = array_keys($defaults); + return $scenarios; } diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 1e5302c..1dd243e 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -586,7 +586,8 @@ abstract class Module extends Component Yii::$app->controller = $oldController; return $result; } else { - throw new InvalidRouteException('Unable to resolve the request "' . trim($this->getUniqueId() . '/' . $route, '/') . '".'); + $id = $this->getUniqueId(); + throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".'); } } @@ -608,9 +609,8 @@ abstract class Module extends Component if ($route === '') { $route = $this->defaultRoute; } - if (($pos = strpos($route, '/')) !== false) { - $id = substr($route, 0, $pos); - $route = substr($route, $pos + 1); + if (strpos($route, '/') !== false) { + list ($id, $route) = explode('/', $route, 2); } else { $id = $route; $route = ''; @@ -623,7 +623,7 @@ abstract class Module extends Component if (isset($this->controllerMap[$id])) { $controller = Yii::createObject($this->controllerMap[$id], $id, $this); - } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { + } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) { $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $id))) . 'Controller'; $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; if (!is_file($classFile)) { diff --git a/framework/yii/db/ActiveRelationTrait.php b/framework/yii/db/ActiveRelationTrait.php index be42eb6..2960196 100644 --- a/framework/yii/db/ActiveRelationTrait.php +++ b/framework/yii/db/ActiveRelationTrait.php @@ -142,35 +142,42 @@ trait ActiveRelationTrait */ private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) { - $buckets = []; - $linkKeys = array_keys($link); - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if ($this->indexBy !== null) { - $buckets[$key][$i] = $model; - } else { - $buckets[$key][] = $model; - } - } - if ($viaModels !== null) { - $viaBuckets = []; + $map = []; $viaLinkKeys = array_keys($viaLink); $linkValues = array_values($link); foreach ($viaModels as $viaModel) { $key1 = $this->getModelKey($viaModel, $viaLinkKeys); $key2 = $this->getModelKey($viaModel, $linkValues); - if (isset($buckets[$key2])) { - foreach ($buckets[$key2] as $i => $bucket) { + $map[$key2][$key1] = true; + } + } + + $buckets = []; + $linkKeys = array_keys($link); + + if (isset($map)) { + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if (isset($map[$key])) { + foreach (array_keys($map[$key]) as $key2) { if ($this->indexBy !== null) { - $viaBuckets[$key1][$i] = $bucket; + $buckets[$key2][$i] = $model; } else { - $viaBuckets[$key1][] = $bucket; + $buckets[$key2][] = $model; } } } } - $buckets = $viaBuckets; + } else { + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } } if (!$this->multiple) { diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index 9327c79..55f4ace 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -141,12 +141,33 @@ class QueryBuilder extends \yii\base\Object * @param array $columns the column names * @param array $rows the rows to be batch inserted into the table * @return string the batch INSERT SQL statement - * @throws NotSupportedException if this is not supported by the underlying DBMS */ public function batchInsert($table, $columns, $rows) { - throw new NotSupportedException($this->db->getDriverName() . ' does not support batch insert.'); + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + } + $values[] = '(' . implode(', ', $vs) . ')'; + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); } /** diff --git a/framework/yii/db/cubrid/QueryBuilder.php b/framework/yii/db/cubrid/QueryBuilder.php index e80e1d6..7682516 100644 --- a/framework/yii/db/cubrid/QueryBuilder.php +++ b/framework/yii/db/cubrid/QueryBuilder.php @@ -69,49 +69,22 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** - * Generates a batch INSERT SQL statement. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the table - * @return string the batch INSERT SQL statement + * @inheritDocs */ - public function batchInsert($table, $columns, $rows) + public function buildLimit($limit, $offset) { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } - - foreach ($columns as $i => $name) { - $columns[$i] = $this->db->quoteColumnName($name); - } - - $values = []; - foreach ($rows as $row) { - $vs = []; - foreach ($row as $i => $value) { - if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->typecast($value); - } - $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + $sql = ''; + // limit is not optional in CUBRID + // http://www.cubrid.org/manual/90/en/LIMIT%20Clause + // "You can specify a very big integer for row_count to display to the last row, starting from a specific row." + if ($limit !== null && $limit >= 0) { + $sql = 'LIMIT ' . (int)$limit; + if ($offset > 0) { + $sql .= ' OFFSET ' . (int)$offset; } - $values[] = '(' . implode(', ', $vs) . ')'; + } elseif ($offset > 0) { + $sql = 'LIMIT ' . (int)$offset . ', 18446744073709551615'; // 2^64-1 } - - return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + return $sql; } } diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php index 50e717c..93a06e3 100644 --- a/framework/yii/db/mysql/QueryBuilder.php +++ b/framework/yii/db/mysql/QueryBuilder.php @@ -142,49 +142,22 @@ class QueryBuilder extends \yii\db\QueryBuilder } /** - * Generates a batch INSERT SQL statement. - * For example, - * - * ~~~ - * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ - * ['Tom', 30], - * ['Jane', 20], - * ['Linda', 25], - * ])->execute(); - * ~~~ - * - * Note that the values in each row must match the corresponding column names. - * - * @param string $table the table that new rows will be inserted into. - * @param array $columns the column names - * @param array $rows the rows to be batch inserted into the table - * @return string the batch INSERT SQL statement + * @inheritDocs */ - public function batchInsert($table, $columns, $rows) + public function buildLimit($limit, $offset) { - if (($tableSchema = $this->db->getTableSchema($table)) !== null) { - $columnSchemas = $tableSchema->columns; - } else { - $columnSchemas = []; - } - - foreach ($columns as $i => $name) { - $columns[$i] = $this->db->quoteColumnName($name); - } - - $values = []; - foreach ($rows as $row) { - $vs = []; - foreach ($row as $i => $value) { - if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->typecast($value); - } - $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + $sql = ''; + // limit is not optional in MySQL + // http://stackoverflow.com/a/271650/1106908 + // http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240 + if ($limit !== null && $limit >= 0) { + $sql = 'LIMIT ' . (int)$limit; + if ($offset > 0) { + $sql .= ' OFFSET ' . (int)$offset; } - $values[] = '(' . implode(', ', $vs) . ')'; + } elseif ($offset > 0) { + $sql = 'LIMIT ' . (int)$offset . ', 18446744073709551615'; // 2^64-1 } - - return 'INSERT INTO ' . $this->db->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + return $sql; } } diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php index 4a5407f..be4248a 100644 --- a/framework/yii/db/sqlite/QueryBuilder.php +++ b/framework/yii/db/sqlite/QueryBuilder.php @@ -42,6 +42,53 @@ class QueryBuilder extends \yii\db\QueryBuilder ]; /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + if (($tableSchema = $this->db->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $this->db->quoteColumnName($name); + } + + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; + } + $values[] = implode(', ', $vs); + } + + return 'INSERT INTO ' . $this->db->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION ALL ', $values); + } + + /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted * will have the specified value or 1. @@ -206,4 +253,23 @@ class QueryBuilder extends \yii\db\QueryBuilder { throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); } + + /** + * @inheritDocs + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + // limit is not optional in SQLite + // http://www.sqlite.org/syntaxdiagrams.html#select-stmt + if ($limit !== null && $limit >= 0) { + $sql = 'LIMIT ' . (int)$limit; + if ($offset > 0) { + $sql .= ' OFFSET ' . (int)$offset; + } + } elseif ($offset > 0) { + $sql = 'LIMIT 9223372036854775807 OFFSET ' . (int)$offset; // 2^63-1 + } + return $sql; + } } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index da8495b..d304741 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -217,6 +217,20 @@ class ModelTest extends TestCase { $singer = new Singer(); $this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); + + $scenarios = [ + 'default' => ['id', 'name', 'description'], + 'administration' => ['name', 'description', 'is_disabled'], + ]; + $model = new ComplexModel1(); + $this->assertEquals($scenarios, $model->scenarios()); + $scenarios = [ + 'default' => ['id', 'name', 'description'], + 'suddenlyUnexpectedScenario' => ['name', 'description'], + 'administration' => ['id', 'name', 'description', 'is_disabled'], + ]; + $model = new ComplexModel2(); + $this->assertEquals($scenarios, $model->scenarios()); } public function testIsAttributeRequired() @@ -234,3 +248,27 @@ class ModelTest extends TestCase $invalid->createValidators(); } } + +class ComplexModel1 extends Model +{ + public function rules() + { + return [ + [['id'], 'required', 'except' => 'administration'], + [['name', 'description'], 'filter', 'filter' => 'trim'], + [['is_disabled'], 'boolean', 'on' => 'administration'], + ]; + } +} + +class ComplexModel2 extends Model +{ + public function rules() + { + return [ + [['id'], 'required', 'except' => 'suddenlyUnexpectedScenario'], + [['name', 'description'], 'filter', 'filter' => 'trim'], + [['is_disabled'], 'boolean', 'on' => 'administration'], + ]; + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php index d7904d0..67cae28 100644 --- a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php +++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php @@ -81,4 +81,10 @@ class SqliteQueryBuilderTest extends QueryBuilderTest $this->setExpectedException('yii\base\NotSupportedException'); parent::testAddDropPrimaryKey(); } + + public function testBatchInsert() + { + $sql = $this->getQueryBuilder()->batchInsert('{{tbl_customer}} t', ['t.id','t.name'], array(array(1,'a'), array(2,'b'))); + $this->assertEquals("INSERT INTO {{tbl_customer}} t ('t'.\"id\", 't'.\"name\") SELECT 1, 'a' UNION ALL 2, 'b'", $sql); + } } diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php index ed5ab33..3d384fc 100644 --- a/tests/unit/framework/i18n/FormatterTest.php +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -77,8 +77,10 @@ class FormatterTest extends TestCase $this->assertSame('$123.00', $this->formatter->asCurrency($value)); $value = '123.456'; $this->assertSame("$123.46", $this->formatter->asCurrency($value)); - $value = '-123456.123'; - $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); + // Starting from ICU 52.1, negative currency value will be formatted as -$123,456.12 + // see: http://source.icu-project.org/repos/icu/icu/tags/release-52-1/source/data/locales/en.txt +// $value = '-123456.123'; +// $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); }