Commit b6b26898 by Carsten Brandt

Merge branch 'master' into elasticsearch

* master: Fixes #1253 Fixes #1310: ActiveRelation does not preserve order of items on find via() and viaTable() fixed limit/offset for sqlite,mysql and cubrid fixed test break. Fixed test break. Fixes #1301: fixed scenario generation when there are "except" scenarios. support for batch insert in sqlite older than 3.7.11 Fixes #1298: supporting route with trailing slash. Fixes #1296: stricter check of dashes in route. Fixes #1307: move batchInsert() to base class. Update apps-advanced.md
parents af5d87ac cf73f40d
...@@ -35,8 +35,8 @@ php /path/to/yii-application/init ...@@ -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`. 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. 3. In `common/config/params.php` set your database details in `components.db` values.
4. Apply migrations with console command 'yii migrate'.
4. Set document roots of your Web server: 5. Set document roots of your Web server:
- for frontend `/path/to/yii-application/frontend/web/` and using the URL `http://frontend/` - 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/` - for backend `/path/to/yii-application/backend/web/` and using the URL `http://backend/`
......
...@@ -165,7 +165,7 @@ class Model extends Component implements IteratorAggregate, ArrayAccess ...@@ -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), * If an attribute should NOT be massively assigned (thus considered unsafe),
* please prefix the attribute with an exclamation character (e.g. '!rank'). * please prefix the attribute with an exclamation character (e.g. '!rank').
* *
...@@ -178,29 +178,49 @@ class Model extends Component implements IteratorAggregate, ArrayAccess ...@@ -178,29 +178,49 @@ class Model extends Component implements IteratorAggregate, ArrayAccess
*/ */
public function scenarios() public function scenarios()
{ {
$scenarios = []; $scenarios = [self::DEFAULT_SCENARIO => []];
$defaults = [];
/** @var $validator Validator */
foreach ($this->getValidators() as $validator) { foreach ($this->getValidators() as $validator) {
if (empty($validator->on)) { 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) { foreach ($validator->attributes as $attribute) {
$defaults[$attribute] = true; $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 { } else {
foreach ($validator->on as $scenario) { foreach ($validator->on as $name) {
foreach ($validator->attributes as $attribute) { foreach ($validator->attributes as $attribute) {
$scenarios[$scenario][$attribute] = true; $scenarios[$name][$attribute] = true;
} }
} }
} }
} }
foreach ($scenarios as $scenario => $attributes) { foreach ($scenarios as $scenario => $attributes) {
foreach (array_keys($defaults) as $attribute) { if (empty($attributes) && $scenario !== self::DEFAULT_SCENARIO) {
$attributes[$attribute] = true; unset($scenarios[$scenario]);
} } else {
$scenarios[$scenario] = array_keys($attributes); $scenarios[$scenario] = array_keys($attributes);
} }
$scenarios[self::DEFAULT_SCENARIO] = array_keys($defaults); }
return $scenarios; return $scenarios;
} }
......
...@@ -586,7 +586,8 @@ abstract class Module extends Component ...@@ -586,7 +586,8 @@ abstract class Module extends Component
Yii::$app->controller = $oldController; Yii::$app->controller = $oldController;
return $result; return $result;
} else { } 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 ...@@ -608,9 +609,8 @@ abstract class Module extends Component
if ($route === '') { if ($route === '') {
$route = $this->defaultRoute; $route = $this->defaultRoute;
} }
if (($pos = strpos($route, '/')) !== false) { if (strpos($route, '/') !== false) {
$id = substr($route, 0, $pos); list ($id, $route) = explode('/', $route, 2);
$route = substr($route, $pos + 1);
} else { } else {
$id = $route; $id = $route;
$route = ''; $route = '';
...@@ -623,7 +623,7 @@ abstract class Module extends Component ...@@ -623,7 +623,7 @@ abstract class Module extends Component
if (isset($this->controllerMap[$id])) { if (isset($this->controllerMap[$id])) {
$controller = Yii::createObject($this->controllerMap[$id], $id, $this); $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'; $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $id))) . 'Controller';
$classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
if (!is_file($classFile)) { if (!is_file($classFile)) {
......
...@@ -142,35 +142,42 @@ trait ActiveRelationTrait ...@@ -142,35 +142,42 @@ trait ActiveRelationTrait
*/ */
private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) 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) { if ($viaModels !== null) {
$viaBuckets = []; $map = [];
$viaLinkKeys = array_keys($viaLink); $viaLinkKeys = array_keys($viaLink);
$linkValues = array_values($link); $linkValues = array_values($link);
foreach ($viaModels as $viaModel) { foreach ($viaModels as $viaModel) {
$key1 = $this->getModelKey($viaModel, $viaLinkKeys); $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
$key2 = $this->getModelKey($viaModel, $linkValues); $key2 = $this->getModelKey($viaModel, $linkValues);
if (isset($buckets[$key2])) { $map[$key2][$key1] = true;
foreach ($buckets[$key2] as $i => $bucket) { }
}
$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) { if ($this->indexBy !== null) {
$viaBuckets[$key1][$i] = $bucket; $buckets[$key2][$i] = $model;
} else { } else {
$viaBuckets[$key1][] = $bucket; $buckets[$key2][] = $model;
}
}
} }
} }
} else {
foreach ($models as $i => $model) {
$key = $this->getModelKey($model, $linkKeys);
if ($this->indexBy !== null) {
$buckets[$key][$i] = $model;
} else {
$buckets[$key][] = $model;
} }
} }
$buckets = $viaBuckets;
} }
if (!$this->multiple) { if (!$this->multiple) {
......
...@@ -141,12 +141,33 @@ class QueryBuilder extends \yii\base\Object ...@@ -141,12 +141,33 @@ class QueryBuilder extends \yii\base\Object
* @param array $columns the column names * @param array $columns the column names
* @param array $rows the rows to be batch inserted into the table * @param array $rows the rows to be batch inserted into the table
* @return string the batch INSERT SQL statement * @return string the batch INSERT SQL statement
* @throws NotSupportedException if this is not supported by the underlying DBMS
*/ */
public function batchInsert($table, $columns, $rows) 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);
} }
/** /**
......
...@@ -69,49 +69,22 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -69,49 +69,22 @@ class QueryBuilder extends \yii\db\QueryBuilder
} }
/** /**
* Generates a batch INSERT SQL statement. * @inheritDocs
* 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) public function buildLimit($limit, $offset)
{ {
if (($tableSchema = $this->db->getTableSchema($table)) !== null) { $sql = '';
$columnSchemas = $tableSchema->columns; // limit is not optional in CUBRID
} else { // http://www.cubrid.org/manual/90/en/LIMIT%20Clause
$columnSchemas = []; // "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;
foreach ($columns as $i => $name) { if ($offset > 0) {
$columns[$i] = $this->db->quoteColumnName($name); $sql .= ' OFFSET ' . (int)$offset;
}
$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; } elseif ($offset > 0) {
$sql = 'LIMIT ' . (int)$offset . ', 18446744073709551615'; // 2^64-1
} }
$values[] = '(' . implode(', ', $vs) . ')'; return $sql;
}
return 'INSERT INTO ' . $this->db->quoteTableName($table)
. ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
} }
} }
...@@ -142,49 +142,22 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -142,49 +142,22 @@ class QueryBuilder extends \yii\db\QueryBuilder
} }
/** /**
* Generates a batch INSERT SQL statement. * @inheritDocs
* 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) public function buildLimit($limit, $offset)
{ {
if (($tableSchema = $this->db->getTableSchema($table)) !== null) { $sql = '';
$columnSchemas = $tableSchema->columns; // limit is not optional in MySQL
} else { // http://stackoverflow.com/a/271650/1106908
$columnSchemas = []; // http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240
} if ($limit !== null && $limit >= 0) {
$sql = 'LIMIT ' . (int)$limit;
foreach ($columns as $i => $name) { if ($offset > 0) {
$columns[$i] = $this->db->quoteColumnName($name); $sql .= ' OFFSET ' . (int)$offset;
} }
} elseif ($offset > 0) {
$values = []; $sql = 'LIMIT ' . (int)$offset . ', 18446744073709551615'; // 2^64-1
foreach ($rows as $row) { }
$vs = []; return $sql;
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);
} }
} }
...@@ -42,6 +42,53 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -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. * 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 * The sequence will be reset such that the primary key of the next new row inserted
* will have the specified value or 1. * will have the specified value or 1.
...@@ -206,4 +253,23 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -206,4 +253,23 @@ class QueryBuilder extends \yii\db\QueryBuilder
{ {
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); 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;
}
} }
...@@ -217,6 +217,20 @@ class ModelTest extends TestCase ...@@ -217,6 +217,20 @@ class ModelTest extends TestCase
{ {
$singer = new Singer(); $singer = new Singer();
$this->assertEquals(['default' => ['lastName', 'underscore_style']], $singer->scenarios()); $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() public function testIsAttributeRequired()
...@@ -234,3 +248,27 @@ class ModelTest extends TestCase ...@@ -234,3 +248,27 @@ class ModelTest extends TestCase
$invalid->createValidators(); $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'],
];
}
}
...@@ -81,4 +81,10 @@ class SqliteQueryBuilderTest extends QueryBuilderTest ...@@ -81,4 +81,10 @@ class SqliteQueryBuilderTest extends QueryBuilderTest
$this->setExpectedException('yii\base\NotSupportedException'); $this->setExpectedException('yii\base\NotSupportedException');
parent::testAddDropPrimaryKey(); 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);
}
} }
...@@ -77,8 +77,10 @@ class FormatterTest extends TestCase ...@@ -77,8 +77,10 @@ class FormatterTest extends TestCase
$this->assertSame('$123.00', $this->formatter->asCurrency($value)); $this->assertSame('$123.00', $this->formatter->asCurrency($value));
$value = '123.456'; $value = '123.456';
$this->assertSame("$123.46", $this->formatter->asCurrency($value)); $this->assertSame("$123.46", $this->formatter->asCurrency($value));
$value = '-123456.123'; // Starting from ICU 52.1, negative currency value will be formatted as -$123,456.12
$this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); // 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)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null));
} }
......
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