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
---
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/`
......
......@@ -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;
}
......
......@@ -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)) {
......
......@@ -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) {
......
......@@ -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);
}
/**
......
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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'],
];
}
}
......@@ -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);
}
}
......@@ -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));
}
......
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