From 1ab7100e5a67349c22c9db74327b1681a9c9fa60 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 03:11:20 +0100 Subject: [PATCH 01/77] updated extension documentation; added sections dependencies, versioning and authorization --- docs/guide/extensions.md | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 47ba380..b4ce8c6 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -4,12 +4,15 @@ Extending Yii Code style ---------- -- Extension code style should be similar to [core framework code style](https://github.com/yiisoft/yii2/wiki/Core-framework-code-style). +- Extension code style SHOULD be similar to [core framework code style](https://github.com/yiisoft/yii2/wiki/Core-framework-code-style). - In case of using getter and setter for defining a property it's preferred to use method in extension code rather than property. -- All classes, methods and properties should be documented using phpdoc. Note that you can use markdown and link to properties and methods +- All classes, methods and properties SHOULD be documented using phpdoc. Note that you can use markdown and link to properties and methods using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. - If you're displaying errors to developers do not translate these (i.e. do not use `\Yii::t()`). Errors should be translated only if they're displayed to end users. +- Extension SHOULD NOT use class prefixes. +- Extension SHOULD provide a valid PSR-0 autoloading configuration in `composer.json` +- Especially for core components (eg. `WebUser`) additional functionality SHOULD be implemented using behaviors or traits instead subclassing. ### Namespace and package names @@ -19,36 +22,57 @@ using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. - Extension MAY use a `yii2-` prefix in the composer vendor name (URL). - Extension MAY use a `yii2-` prefix in the repository name (URL). +### Dependencies + +- Additional code, eg. libraries, SHOULD be required in your `composer.json` file. +- Requirements SHOULD NOT include `dev` packages without a `stable` release. +- Use appropriate version constraints, eg. `1.*`, `@stable` for requirements. + +### Versioning + +- Extension SHOULD follow the rules of [semantic versioning](http://semver.org). +- Use a consistent format for your repository tags, as they are treated as version strings by composer, eg. `0.2.4`,`0.2.5`,`0.3.0`,`1.0.0`. + Distribution ------------ - There should be a `readme.md` file clearly describing what extension does in English, its requirements, how to install and use it. It should be written using markdown. If you want to provide translated readme, name it as `readme_ru.md` where `ru` is your language code. If extension provides a widget it is a good idea to include some screenshots. +- It is recommended to host your extensions at [Github](github.com). +- Extension MUST be registered at [Packagist](https://packagist.org). - TBD: composer.json -- It is recommended to host your extensions at github.com. Working with database --------------------- - If extension creates or modifies database schema always use Yii migrations instead of SQL files or custom scripts. +- Migrations SHOULD be database agnostic. +- You MUST NOT make use of active-record model classes in your migrations. Assets ------ -TBD +- Asset files MUST be registered through Bundles. Events ------ -TBD +- Extension SHOULD make use of the event system, in favor of providing too complex configuration options. i18n ---- -TBD +- Extension SHOULD provide at least one message catalogue with either source or target language in English. +- Extension MAY provide a configuration for creating message catalogues. + +Authorization +------------- + +- Auth-items for controllers SHOULD be named after the following format `vendor\ext\controller\action`. +- Auth-items names may be shortened using an asterisk, eg. `vendor\ext\*` Testing your extension ---------------------- -TBD \ No newline at end of file +- Extension SHOULD be testable with *Codeception*. \ No newline at end of file From e748455d8f95484408f7816157c677216c01b503 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 03:45:47 +0100 Subject: [PATCH 02/77] reverted events --- docs/guide/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index b4ce8c6..202deb3 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -58,7 +58,7 @@ Assets Events ------ -- Extension SHOULD make use of the event system, in favor of providing too complex configuration options. +TBD i18n ---- From 857feeb6ac0ee8090a8cbd67e93c350c38434cdd Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 12:46:15 +0100 Subject: [PATCH 03/77] updated code style, added autoloading and prefix examples --- docs/guide/extensions.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 202deb3..e20b4e3 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -10,9 +10,34 @@ Code style using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. - If you're displaying errors to developers do not translate these (i.e. do not use `\Yii::t()`). Errors should be translated only if they're displayed to end users. -- Extension SHOULD NOT use class prefixes. -- Extension SHOULD provide a valid PSR-0 autoloading configuration in `composer.json` -- Especially for core components (eg. `WebUser`) additional functionality SHOULD be implemented using behaviors or traits instead subclassing. +- Extension SHOULD NOT use class prefixes (i.e. `TbNavBar`, `EMyWidget`, etc.) +- Extension SHOULD provide a valid PSR-0 autoloading configuration in `composer.json` + + **Example 1: Code in repository root** + + ``` + ./Class.php + ``` + + ``` + "autoload": { + "psr-0": { "vendor\\package\\": "" } + }, + ``` + + **Example 2: Code in repository subfolder `./src`** + + ``` + ./src/vendor/package/Class.php + ``` + + ``` + "autoload": { + "psr-0": { "vendor\\package\\": "./src" } + }, + ``` + + ### Namespace and package names From ce5c3695de84133340b4227dbd249f5a7e915b6d Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 12:49:21 +0100 Subject: [PATCH 04/77] updated dependencies and testing --- docs/guide/extensions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index e20b4e3..d1fcb00 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -50,7 +50,7 @@ using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. ### Dependencies - Additional code, eg. libraries, SHOULD be required in your `composer.json` file. -- Requirements SHOULD NOT include `dev` packages without a `stable` release. +- When extension is released in a stable version, its requirements SHOULD NOT include `dev` packages that do not have a `stable` release. - Use appropriate version constraints, eg. `1.*`, `@stable` for requirements. ### Versioning @@ -100,4 +100,4 @@ Authorization Testing your extension ---------------------- -- Extension SHOULD be testable with *Codeception*. \ No newline at end of file +- Extension SHOULD be testable with *PHPUnit*. \ No newline at end of file From 93cc73858fb138bb972b69adbe7007e42d0b26e7 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 14:21:11 +0100 Subject: [PATCH 05/77] added link to composer docs about autoloading --- docs/guide/extensions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index d1fcb00..1f877e6 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -36,6 +36,8 @@ using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. "psr-0": { "vendor\\package\\": "./src" } }, ``` + + Details about autoloading configuration can be found in the [composer documentation](http://getcomposer.org/doc/04-schema.md#autoload). From 5deb76120aa503e97fe0ae1dbb5087b7a8f214a2 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Thu, 12 Dec 2013 21:58:14 +0100 Subject: [PATCH 06/77] changed install to create-project, added hint --- docs/internals/getting-started.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/internals/getting-started.md b/docs/internals/getting-started.md index bcaac07..8a5e506 100644 --- a/docs/internals/getting-started.md +++ b/docs/internals/getting-started.md @@ -7,7 +7,7 @@ Composer package. Here's how to do it: 1. `git clone git@github.com:yiisoft/yii2-app-basic.git`. 2. Remove `.git` directory from cloned directory. 3. Change `composer.json`. Instead of all stable requirements add just one `"yiisoft/yii2-dev": "*"`. -4. Execute `composer install`. +4. Execute `composer create-project`. 5. Now you have working playground that uses latest code. If you're core developer there's no extra step needed. You can change framework code under @@ -23,3 +23,5 @@ If you're not core developer or want to use your own fork for pull requests: [remote "origin"] url = git://github.com/username/yii2.git ``` + +> Hint: The workflow of forking a package and pushing changes back into your fork and then sending a pull-request to the maintainer is the same for all extensions you require via composer. \ No newline at end of file From 124a73a59801d36f065e93d5443c96876506704f Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 17 Dec 2013 15:51:27 +0100 Subject: [PATCH 07/77] make Query reuseable fixes #1545 --- framework/yii/db/ActiveQuery.php | 13 +++++++++++-- framework/yii/db/Query.php | 30 ++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 52dbcdb..e93e9be 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -123,6 +123,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface } if ($this->sql === null) { + $select = $this->select; + $from = $this->from; + if ($this->from === null) { $tableName = $modelClass::tableName(); if ($this->select === null && !empty($this->join)) { @@ -130,8 +133,14 @@ class ActiveQuery extends Query implements ActiveQueryInterface } $this->from = [$tableName]; } - list ($this->sql, $this->params) = $db->getQueryBuilder()->build($this); + list ($sql, $params) = $db->getQueryBuilder()->build($this); + + $this->select = $select; + $this->from = $from; + } else { + $sql = $this->sql; + $params = $this->params; } - return $db->createCommand($this->sql, $this->params); + return $db->createCommand($sql, $params); } } diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 301b2e1..fb1d42f 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -190,8 +190,11 @@ class Query extends Component implements QueryInterface */ public function count($q = '*', $db = null) { + $select = $this->select; $this->select = ["COUNT($q)"]; - return $this->createCommand($db)->queryScalar(); + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar(); } /** @@ -204,8 +207,11 @@ class Query extends Component implements QueryInterface */ public function sum($q, $db = null) { + $select = $this->select; $this->select = ["SUM($q)"]; - return $this->createCommand($db)->queryScalar(); + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar(); } /** @@ -218,8 +224,11 @@ class Query extends Component implements QueryInterface */ public function average($q, $db = null) { + $select = $this->select; $this->select = ["AVG($q)"]; - return $this->createCommand($db)->queryScalar(); + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar(); } /** @@ -232,8 +241,11 @@ class Query extends Component implements QueryInterface */ public function min($q, $db = null) { + $select = $this->select; $this->select = ["MIN($q)"]; - return $this->createCommand($db)->queryScalar(); + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar(); } /** @@ -246,8 +258,11 @@ class Query extends Component implements QueryInterface */ public function max($q, $db = null) { + $select = $this->select; $this->select = ["MAX($q)"]; - return $this->createCommand($db)->queryScalar(); + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar(); } /** @@ -258,8 +273,11 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { + $select = $this->select; $this->select = [new Expression('1')]; - return $this->scalar($db) !== false; + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar() !== false; } /** From b2d9166927d6e5156ef2d1d037dd08517fa87086 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 17 Dec 2013 16:14:27 +0100 Subject: [PATCH 08/77] refactored scalar query functions to share common code --- framework/yii/db/Query.php | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index fb1d42f..0d81887 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -190,11 +190,7 @@ class Query extends Component implements QueryInterface */ public function count($q = '*', $db = null) { - $select = $this->select; - $this->select = ["COUNT($q)"]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar(); + return $this->queryScalar("COUNT($q)", $db); } /** @@ -207,11 +203,7 @@ class Query extends Component implements QueryInterface */ public function sum($q, $db = null) { - $select = $this->select; - $this->select = ["SUM($q)"]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar(); + return $this->queryScalar("SUM($q)", $db); } /** @@ -224,11 +216,7 @@ class Query extends Component implements QueryInterface */ public function average($q, $db = null) { - $select = $this->select; - $this->select = ["AVG($q)"]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar(); + return $this->queryScalar("AVG($q)", $db); } /** @@ -241,11 +229,7 @@ class Query extends Component implements QueryInterface */ public function min($q, $db = null) { - $select = $this->select; - $this->select = ["MIN($q)"]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar(); + return $this->queryScalar("MIN($q)", $db); } /** @@ -258,11 +242,7 @@ class Query extends Component implements QueryInterface */ public function max($q, $db = null) { - $select = $this->select; - $this->select = ["MAX($q)"]; - $command = $this->createCommand($db); - $this->select = $select; - return $command->queryScalar(); + return $this->queryScalar("MAX($q)", $db); } /** @@ -273,11 +253,23 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { + return $this->queryScalar(new Expression('1'), $db) !== false; + } + + /** + * Queries a scalar value by setting [[select]] first. + * Restores the value of select to make this query reusable. + * @param string|Expression $selectExpression + * @param Connection $db + * @return bool|string + */ + private function queryScalar($selectExpression, $db) + { $select = $this->select; - $this->select = [new Expression('1')]; + $this->select = [$selectExpression]; $command = $this->createCommand($db); $this->select = $select; - return $command->queryScalar() !== false; + return $command->queryScalar(); } /** From 7a81110f6bcf60a4810bebc53607ed1d9a8342a1 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 17 Dec 2013 17:01:01 +0100 Subject: [PATCH 09/77] make ActiveRelation reusable fixes #1560 --- framework/yii/db/ActiveRelation.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index b016c5c..0659ee3 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -63,6 +63,7 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface public function createCommand($db = null) { if ($this->primaryModel !== null) { + $where = $this->where; // lazy loading if ($this->via instanceof self) { // via pivot table @@ -84,6 +85,9 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface } else { $this->filterByModels([$this->primaryModel]); } + $command = parent::createCommand($db); + $this->where = $where; + return $command; } return parent::createCommand($db); } From 0a7def2055c2f2e68655a6004f0d59e3e78855b6 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 17 Dec 2013 17:24:23 +0100 Subject: [PATCH 10/77] updated codeception docs. fixes #1558 --- apps/basic/tests/README.md | 7 +++ extensions/yii/codeception/README.md | 116 +++++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 46 deletions(-) diff --git a/apps/basic/tests/README.md b/apps/basic/tests/README.md index 117ab5b..5ff2669 100644 --- a/apps/basic/tests/README.md +++ b/apps/basic/tests/README.md @@ -11,6 +11,13 @@ To run the tests, follow these steps: - `functional.suite.yml` for [functional testing](http://codeception.com/docs/05-FunctionalTests) and `unit.suite.yml` for [unit testing](http://codeception.com/docs/06-UnitTests) should already work out of the box and should not need to be adjusted. + - If you want to run acceptance tests, you need to download [selenium standalone](http://www.seleniumhq.org/download/) + and start it with command `java -jar {selenium-standalone-name}.jar`. + After that you can use `WebDriver` codeception module that will connect to selenium and launch browser. + This also allows you to use [Xvfb](https://en.wikipedia.org/wiki/Xvfb) in your tests which allows you to run tests + without showing the running browser on the screen. There is codeception [blog post](http://codeception.com/05-24-2013/jenkins-ci-practice.html) + that explains how it works. + 3. Go to the application base directory and build the test suites: ``` php codecept.phar build // rebuild test scripts, only need to be run once diff --git a/extensions/yii/codeception/README.md b/extensions/yii/codeception/README.md index 9550387..6df55e3 100644 --- a/extensions/yii/codeception/README.md +++ b/extensions/yii/codeception/README.md @@ -1,33 +1,78 @@ Codeception Extension for Yii 2 =============================== -This extension provides a `Codeception` mail solution for Yii 2. It includes some classes that are useful -for unit-testing (```TestCase```) or for codeception page-objects (```BasePage```). +This extension provides [Codeception](http://codeception.com/) integration for the Yii Framework 2.0. -When using codeception page-objects they have some similar code, this code was extracted and put into the ```BasePage``` -class to reduce code duplication. Simply extend your page object from this class, like it is done in ```yii2-basic``` and -```yii2-advanced``` boilerplates. +It provides classes that help with testing with codeception: -For unit testing there is a ```TestCase``` class which holds some common features like application creation before each test -and application destroy after each test. You can configure your application by this class. ```TestCase``` is extended from ```PHPUnit_Framework_TestCase``` so all -methods and assertions are available. +- a base class for unit-tests: `yii\codeception\TestCase +- a base class for codeception page-objects: `yii\codeception\BasePage`. +- a solution for testing emails + + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require yiisoft/yii2-codeception "*" +``` + +or add + +```json +"yiisoft/yii2-codeception": "*" +``` + +to the require section of your composer.json. + + +Usage +----- + +When using codeception page-objects they have some similar code, this code was extracted and put into the `BasePage` +class to reduce code duplication. Simply extend your page object from this class, like it is done in `yii2-app-basic` and +`yii2-app-advanced` boilerplates. + +For unit testing there is a `TestCase` class which holds some common features like application creation before each test +and application destroy after each test. You can configure a mock application using this class. +`TestCase` is extended from `PHPUnit_Framework_TestCase` so all methods and assertions are available. ```php -SomeConsoleTest extends yii\codeception\TestCase + [ @@ -39,19 +84,19 @@ SomeOtherTest extends yii\codeception\TestCase } ``` -Because of Codeception buffers all output you cant make simple ```var_dump()``` in the TestCase, instead you need to use -```Codeception\Util\Debug::debug()``` function and then run test with ```--debug``` key, for example: +Because of Codeception buffers all output you can't make simple `var_dump()` in the TestCase, instead you need to use +`Codeception\Util\Debug::debug()` function and then run test with `--debug` key, for example: ```php + Date: Tue, 17 Dec 2013 18:27:38 +0100 Subject: [PATCH 11/77] removed based on samdark's comment from https://github.com/yiisoft/yii2/pull/1485/files#r8293528 --- docs/guide/extensions.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 1f877e6..c4703f3 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -5,7 +5,6 @@ Code style ---------- - Extension code style SHOULD be similar to [core framework code style](https://github.com/yiisoft/yii2/wiki/Core-framework-code-style). -- In case of using getter and setter for defining a property it's preferred to use method in extension code rather than property. - All classes, methods and properties SHOULD be documented using phpdoc. Note that you can use markdown and link to properties and methods using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. - If you're displaying errors to developers do not translate these (i.e. do not use `\Yii::t()`). Errors should be From ffe4867d668adb9fdf08916064682b3410bc99b3 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Tue, 17 Dec 2013 18:28:11 +0100 Subject: [PATCH 12/77] updated autoloading info --- docs/guide/extensions.md | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index c4703f3..65546b1 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -10,33 +10,7 @@ using the following syntax: e.g. `[[name()]]`, `[[name\space\MyClass::name()]]`. - If you're displaying errors to developers do not translate these (i.e. do not use `\Yii::t()`). Errors should be translated only if they're displayed to end users. - Extension SHOULD NOT use class prefixes (i.e. `TbNavBar`, `EMyWidget`, etc.) -- Extension SHOULD provide a valid PSR-0 autoloading configuration in `composer.json` - - **Example 1: Code in repository root** - - ``` - ./Class.php - ``` - - ``` - "autoload": { - "psr-0": { "vendor\\package\\": "" } - }, - ``` - - **Example 2: Code in repository subfolder `./src`** - - ``` - ./src/vendor/package/Class.php - ``` - - ``` - "autoload": { - "psr-0": { "vendor\\package\\": "./src" } - }, - ``` - - Details about autoloading configuration can be found in the [composer documentation](http://getcomposer.org/doc/04-schema.md#autoload). +- Extension MUST provide a valid autoloading configuration in `composer.json`. Details can be found in the [composer documentation](http://getcomposer.org/doc/04-schema.md#autoload) or see all [official Yii2 extensions](https://github.com/yiisoft/yii2/tree/master/extensions/yii) for example. From 249243a7b0580ce39a3fbe6f7d9c0dd97b983e11 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Tue, 17 Dec 2013 19:14:38 +0100 Subject: [PATCH 13/77] updated database, based on @cebe's comment https://github.com/yiisoft/yii2/pull/1485#discussion_r8410809 --- docs/guide/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 65546b1..4f178ef 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -47,7 +47,7 @@ Working with database --------------------- - If extension creates or modifies database schema always use Yii migrations instead of SQL files or custom scripts. -- Migrations SHOULD be database agnostic. +- Migrations SHOULD be DBMS agnostic. - You MUST NOT make use of active-record model classes in your migrations. Assets From 12e6c9bc29109b3c6d104691d872236b17556e22 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 17 Dec 2013 20:05:19 +0100 Subject: [PATCH 14/77] Fixes #1522: added example of executing extra queries right after establishing DB connection --- docs/guide/database-basics.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index 520da22..9354a3d 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -72,6 +72,26 @@ $connection->open(); ``` +> **Tip**: if you need to execute additional SQL queries right after establishing a connection you can add the +> following to your application configuration file: +> +```php +return [ + // ... + 'components' => [ + // ... + 'db' => [ + 'class' => 'yii\db\Connection', + // ... + 'on afterOpen' => function($event) { + $event->sender->createCommand("SET time_zone = 'UTC'")->execute(); + } + ], + ], + // ... +]; +``` + Basic SQL queries ----------------- From 4839ac22e29fb9e45522421937a5026a46c62456 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 17 Dec 2013 20:11:38 +0100 Subject: [PATCH 15/77] Fixes #1381: better description of Apache config required to hide index.php --- docs/guide/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 6a99458..eab0046 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -96,7 +96,8 @@ By default, requests for pages in a Yii-based site go through the bootstrap file in the application's `web` directory. The result will be URLs in the format `http://hostname/index.php/controller/action/param/value`. To hide the bootstrap file in your URLs, add `mod_rewrite` instructions to the `.htaccess` file in your web document root -(or add the instructions to the virtual host configuration in Apache's `httpd.conf` file). The applicable instructions are: +(or add the instructions to the virtual host configuration in Apache's `httpd.conf` file, `Directory` section for your webroot). +The applicable instructions are: ~~~ RewriteEngine on From 382cee156c9f3cded7b9e5f7b258078f76014e00 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 00:03:40 +0100 Subject: [PATCH 16/77] Made yii2-dev composer.json more lightweight Do not depend on all packages, suggest them instead. require-dev will still have them for testing. --- composer.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 75ec50c..b468a43 100644 --- a/composer.json +++ b/composer.json @@ -68,16 +68,32 @@ "php": ">=5.4.0", "ext-mbstring": "*", "lib-pcre": "*", - "yiisoft/jquery": "1.10.*", "yiisoft/yii2-composer": "*", + "yiisoft/jquery": "1.10.*", "phpspec/php-diff": ">=1.0.2", "ezyang/htmlpurifier": "4.5.*", - "michelf/php-markdown": "1.3.*", + "michelf/php-markdown": "1.3.*" + }, + "require-dev": { "twbs/bootstrap": "3.0.*", + "ext-curl": "*", + "ext-mongo": ">=1.3.0", + "ext-pdo": "*", + "ext-pdo_mysql": "*", "smarty/smarty": "*", "swiftmailer/swiftmailer": "*", "twig/twig": "*" }, + "suggest": { + "twbs/bootstrap": "required by yii2-bootstrap, yii2-debug, yii2-gii extension", + "ext-curl": "required by yii2-elasticsearch extension", + "ext-mongo": "required by yii2-mongo extension", + "ext-pdo": "required by yii2-sphinx extension", + "ext-pdo_mysql": "required by yii2-sphinx extension", + "smarty/smarty": "required by yii2-smarty extension", + "swiftmailer/swiftmailer": "required by yii2-swiftmailer extension", + "twig/twig": "required by yii2-twig extension" + }, "autoload": { "psr-0": { "yii\\bootstrap\\": "extensions/", From 5036a1e1225f750ae64237f3e78f1ca97d6d33ba Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 00:07:31 +0100 Subject: [PATCH 17/77] Added note about changes in composer.json for yii2-dev --- docs/internals/getting-started.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/internals/getting-started.md b/docs/internals/getting-started.md index bcaac07..7fb9772 100644 --- a/docs/internals/getting-started.md +++ b/docs/internals/getting-started.md @@ -10,6 +10,10 @@ Composer package. Here's how to do it: 4. Execute `composer install`. 5. Now you have working playground that uses latest code. +Note that requirements of extensions that come with `yii2-dev` are not loaded automatically. +If you want to use an extension, check if there are dependencies suggested for it and add them +to your `composer.json`. + If you're core developer there's no extra step needed. You can change framework code under `vendor/yiisoft/yii2-dev` and push it to main repository. From 85a15424bd1d84525bd05ddc19ff157f5cb0e829 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 00:35:19 +0100 Subject: [PATCH 18/77] typo in bootstrap composer.json --- extensions/yii/bootstrap/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/bootstrap/composer.json b/extensions/yii/bootstrap/composer.json index 3e6031e..32e9681 100644 --- a/extensions/yii/bootstrap/composer.json +++ b/extensions/yii/bootstrap/composer.json @@ -19,7 +19,7 @@ ], "require": { "yiisoft/yii2": "*", - "twbs/bootstrap": "3.0.*" + "twbs/bootstrap": "v3.0.*" }, "autoload": { "psr-0": { "yii\\bootstrap\\": "" } From 2b2a000ff51d77e8bac229864b2d1f139275fe18 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 00:46:09 +0100 Subject: [PATCH 19/77] added mongodb to travis --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 127e5a0..716bb48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,18 +9,22 @@ services: - redis-server - memcached - elasticsearch + - mongodb -before_script: +install: + - pecl -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` - composer self-update && composer --version - - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - - mysql -e 'CREATE DATABASE yiitest;'; - - psql -U postgres -c 'CREATE DATABASE yiitest;'; +# - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - echo 'elasticsearch version ' && curl http://localhost:9200/ - tests/unit/data/travis/apc-setup.sh - tests/unit/data/travis/memcache-setup.sh - tests/unit/data/travis/cubrid-setup.sh - tests/unit/data/travis/sphinx-setup.sh +before_script: + - mysql -e 'CREATE DATABASE yiitest;'; + - psql -U postgres -c 'CREATE DATABASE yiitest;'; + script: - phpunit --coverage-text --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor From 119abd1dfc57d0aabcf05f44b87fd68207d1cfb1 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 00:51:50 +0100 Subject: [PATCH 20/77] Update getting-started.md --- docs/internals/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/getting-started.md b/docs/internals/getting-started.md index 7fb9772..0d211a5 100644 --- a/docs/internals/getting-started.md +++ b/docs/internals/getting-started.md @@ -12,7 +12,7 @@ Composer package. Here's how to do it: Note that requirements of extensions that come with `yii2-dev` are not loaded automatically. If you want to use an extension, check if there are dependencies suggested for it and add them -to your `composer.json`. +to your `composer.json`. You can see suggested packages by running `composer show yiisoft/yii2-dev`. If you're core developer there's no extra step needed. You can change framework code under `vendor/yiisoft/yii2-dev` and push it to main repository. From 89a5edb9138cbdd684061cb4793663a8ea260bec Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 01:01:27 +0100 Subject: [PATCH 21/77] pecl: do not fail if already installed --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 716bb48..722a016 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ services: - mongodb install: - - pecl -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` + - pecl -s -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` - composer self-update && composer --version # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - echo 'elasticsearch version ' && curl http://localhost:9200/ From 90c99c94b90539862b69d3a9658647bc2776cdbd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 01:09:32 +0100 Subject: [PATCH 22/77] pecl fails when installed, even with -s wtf? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 722a016..929de28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ services: - mongodb install: - - pecl -s -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` +# - pecl -s -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` - composer self-update && composer --version # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - echo 'elasticsearch version ' && curl http://localhost:9200/ From 596345bdb2de18afc084abfc0614b1f8eda64135 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 01:24:31 +0100 Subject: [PATCH 23/77] getting mongo right on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 929de28..f4c81cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ services: - mongodb install: -# - pecl -s -q install mongo && echo "extension=mongo.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` - composer self-update && composer --version # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - echo 'elasticsearch version ' && curl http://localhost:9200/ @@ -20,6 +19,7 @@ install: - tests/unit/data/travis/memcache-setup.sh - tests/unit/data/travis/cubrid-setup.sh - tests/unit/data/travis/sphinx-setup.sh + - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini before_script: - mysql -e 'CREATE DATABASE yiitest;'; From 514a825d02c6d63c271f7d81fb21ddf29fcd8010 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 02:05:00 +0100 Subject: [PATCH 24/77] travis: create mongo database and add user --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f4c81cc..41369b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ install: before_script: - mysql -e 'CREATE DATABASE yiitest;'; - psql -U postgres -c 'CREATE DATABASE yiitest;'; + - mongo yii2test --eval 'db.addUser("travis", "test");' script: - phpunit --coverage-text --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor From 6bf6d2f4e75be6aac954fce8c69b8e37caaaf0c7 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 02:51:30 +0100 Subject: [PATCH 25/77] enable mongo textsearch on travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 41369b7..fc3be7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,8 @@ install: - tests/unit/data/travis/cubrid-setup.sh - tests/unit/data/travis/sphinx-setup.sh - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - echo "textSearchEnabled=true" >> /etc/mongodb.conf + - sudo service restart mongodb before_script: - mysql -e 'CREATE DATABASE yiitest;'; From 4f155c9e49b0a537270f221ee94a4d49887b87ad Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 03:14:43 +0100 Subject: [PATCH 26/77] sudo! --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fc3be7b..69ba417 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ install: - tests/unit/data/travis/cubrid-setup.sh - tests/unit/data/travis/sphinx-setup.sh - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - echo "textSearchEnabled=true" >> /etc/mongodb.conf + - sudo echo "textSearchEnabled=true" >> /etc/mongodb.conf - sudo service restart mongodb before_script: From e36f7a63c574bbeb4bc7b313db01701df59255bc Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 03:19:54 +0100 Subject: [PATCH 27/77] better sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 69ba417..beb7776 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ install: - tests/unit/data/travis/cubrid-setup.sh - tests/unit/data/travis/sphinx-setup.sh - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - sudo echo "textSearchEnabled=true" >> /etc/mongodb.conf + - sudo 'echo "textSearchEnabled=true" >> /etc/mongodb.conf' - sudo service restart mongodb before_script: From 4d1390ceb1690230d53bc41cd4d8997ab1cfc728 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 03:41:19 +0100 Subject: [PATCH 28/77] fixed travis mongodb --- .travis.yml | 6 ++---- tests/unit/data/travis/mongodb-setup.sh | 11 +++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100755 tests/unit/data/travis/mongodb-setup.sh diff --git a/.travis.yml b/.travis.yml index beb7776..da19dcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,17 +15,15 @@ install: - composer self-update && composer --version # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - echo 'elasticsearch version ' && curl http://localhost:9200/ + - tests/unit/data/travis/mongodb-setup.sh - tests/unit/data/travis/apc-setup.sh - tests/unit/data/travis/memcache-setup.sh - tests/unit/data/travis/cubrid-setup.sh - - tests/unit/data/travis/sphinx-setup.sh - - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - sudo 'echo "textSearchEnabled=true" >> /etc/mongodb.conf' - - sudo service restart mongodb before_script: - mysql -e 'CREATE DATABASE yiitest;'; - psql -U postgres -c 'CREATE DATABASE yiitest;'; + - tests/unit/data/travis/sphinx-setup.sh - mongo yii2test --eval 'db.addUser("travis", "test");' script: diff --git a/tests/unit/data/travis/mongodb-setup.sh b/tests/unit/data/travis/mongodb-setup.sh new file mode 100755 index 0000000..b79a897 --- /dev/null +++ b/tests/unit/data/travis/mongodb-setup.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# +# install mongodb + +echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +sudo sh -c 'echo "setParameter = textSearchEnabled=true" >> /etc/mongodb.conf' +cat /etc/mongodb.conf + +mongod --version + +sudo service mongodb restart From cf61967d7650950e0b892b02b7feab051bf1de6a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 17 Dec 2013 23:43:43 -0500 Subject: [PATCH 29/77] Refactored codeception/BasePage. --- apps/basic/tests/_pages/AboutPage.php | 4 +- apps/basic/tests/_pages/ContactPage.php | 2 +- apps/basic/tests/_pages/LoginPage.php | 2 +- apps/basic/tests/acceptance/AboutCept.php | 2 +- apps/basic/tests/acceptance/ContactCept.php | 4 +- apps/basic/tests/acceptance/LoginCept.php | 4 +- apps/basic/tests/functional/AboutCept.php | 2 +- apps/basic/tests/functional/ContactCept.php | 4 +- apps/basic/tests/functional/LoginCept.php | 4 +- extensions/yii/codeception/BasePage.php | 64 ++++++++++++++++++----------- 10 files changed, 55 insertions(+), 37 deletions(-) diff --git a/apps/basic/tests/_pages/AboutPage.php b/apps/basic/tests/_pages/AboutPage.php index ed5eb1b..5f9021f 100644 --- a/apps/basic/tests/_pages/AboutPage.php +++ b/apps/basic/tests/_pages/AboutPage.php @@ -6,5 +6,5 @@ use yii\codeception\BasePage; class AboutPage extends BasePage { - public static $URL = '?r=site/about'; -} \ No newline at end of file + public $route = 'site/about'; +} diff --git a/apps/basic/tests/_pages/ContactPage.php b/apps/basic/tests/_pages/ContactPage.php index 8149436..4fd6fa8 100644 --- a/apps/basic/tests/_pages/ContactPage.php +++ b/apps/basic/tests/_pages/ContactPage.php @@ -6,7 +6,7 @@ use yii\codeception\BasePage; class ContactPage extends BasePage { - public static $URL = '?r=site/contact'; + public $route = 'site/contact'; /** * contact form name text field locator diff --git a/apps/basic/tests/_pages/LoginPage.php b/apps/basic/tests/_pages/LoginPage.php index 74725d1..8493d51 100644 --- a/apps/basic/tests/_pages/LoginPage.php +++ b/apps/basic/tests/_pages/LoginPage.php @@ -6,7 +6,7 @@ use yii\codeception\BasePage; class LoginPage extends BasePage { - public static $URL = '?r=site/login'; + public $route = 'site/login'; /** * login form username text field locator diff --git a/apps/basic/tests/acceptance/AboutCept.php b/apps/basic/tests/acceptance/AboutCept.php index b6be2f3..deecee7 100644 --- a/apps/basic/tests/acceptance/AboutCept.php +++ b/apps/basic/tests/acceptance/AboutCept.php @@ -4,5 +4,5 @@ use tests\_pages\AboutPage; $I = new WebGuy($scenario); $I->wantTo('ensure that about works'); -$I->amOnPage(AboutPage::$URL); +AboutPage::openBy($I); $I->see('About', 'h1'); diff --git a/apps/basic/tests/acceptance/ContactCept.php b/apps/basic/tests/acceptance/ContactCept.php index 107d12d..25f5735 100644 --- a/apps/basic/tests/acceptance/ContactCept.php +++ b/apps/basic/tests/acceptance/ContactCept.php @@ -4,9 +4,9 @@ use tests\_pages\ContactPage; $I = new WebGuy($scenario); $I->wantTo('ensure that contact works'); -$contactPage = ContactPage::of($I); -$I->amOnPage(ContactPage::$URL); +$contactPage = ContactPage::openBy($I); + $I->see('Contact', 'h1'); $I->amGoingTo('submit contact form with no data'); diff --git a/apps/basic/tests/acceptance/LoginCept.php b/apps/basic/tests/acceptance/LoginCept.php index b6ce5f9..5d6a387 100644 --- a/apps/basic/tests/acceptance/LoginCept.php +++ b/apps/basic/tests/acceptance/LoginCept.php @@ -4,9 +4,9 @@ use tests\_pages\LoginPage; $I = new WebGuy($scenario); $I->wantTo('ensure that login works'); -$loginPage = LoginPage::of($I); -$I->amOnPage(LoginPage::$URL); +$loginPage = LoginPage::openBy($I); + $I->see('Login', 'h1'); $I->amGoingTo('try to login with empty credentials'); diff --git a/apps/basic/tests/functional/AboutCept.php b/apps/basic/tests/functional/AboutCept.php index 419b3fe..1875c2e 100644 --- a/apps/basic/tests/functional/AboutCept.php +++ b/apps/basic/tests/functional/AboutCept.php @@ -4,5 +4,5 @@ use tests\_pages\AboutPage; $I = new TestGuy($scenario); $I->wantTo('ensure that about works'); -$I->amOnPage(AboutPage::$URL); +AboutPage::openBy($I); $I->see('About', 'h1'); diff --git a/apps/basic/tests/functional/ContactCept.php b/apps/basic/tests/functional/ContactCept.php index 9c7cab3..ddc7ca6 100644 --- a/apps/basic/tests/functional/ContactCept.php +++ b/apps/basic/tests/functional/ContactCept.php @@ -4,9 +4,9 @@ use tests\functional\_pages\ContactPage; $I = new TestGuy($scenario); $I->wantTo('ensure that contact works'); -$contactPage = ContactPage::of($I); -$I->amOnPage(ContactPage::$URL); +$contactPage = ContactPage::openBy($I); + $I->see('Contact', 'h1'); $I->amGoingTo('submit contact form with no data'); diff --git a/apps/basic/tests/functional/LoginCept.php b/apps/basic/tests/functional/LoginCept.php index 7a76e18..770b823 100644 --- a/apps/basic/tests/functional/LoginCept.php +++ b/apps/basic/tests/functional/LoginCept.php @@ -4,9 +4,9 @@ use tests\functional\_pages\LoginPage; $I = new TestGuy($scenario); $I->wantTo('ensure that login works'); -$loginPage = LoginPage::of($I); -$I->amOnPage(LoginPage::$URL); +$loginPage = LoginPage::openBy($I); + $I->see('Login', 'h1'); $I->amGoingTo('try to login with empty credentials'); diff --git a/extensions/yii/codeception/BasePage.php b/extensions/yii/codeception/BasePage.php index d2e3b77..3a9f8f8 100644 --- a/extensions/yii/codeception/BasePage.php +++ b/extensions/yii/codeception/BasePage.php @@ -2,54 +2,72 @@ namespace yii\codeception; +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; + /** - * Represents a web page to test - * - * Pages extend from this class and declare UI map for this page via - * static properties. CSS or XPath allowed. - * - * Here is an example: + * BasePage is the base class for page classes that represent Web pages to be tested. * - * ```php - * public static $usernameField = '#username'; - * public static $formSubmitButton = "#mainForm input[type=submit]"; - * ``` + * @property string $url the URL to this page * * @author Mark Jebri * @since 2.0 */ -abstract class BasePage +abstract class BasePage extends Component { /** - * @var string include url of current page. This property has to be overwritten by subclasses + * @var string|array the route (controller ID and action ID, e.g. `site/about`) to this page. + * Use array to represent a route with GET parameters. The first element of the array represents + * the route and the rest of the name-value pairs are treated as GET parameters, e.g. `array('site/page', 'name' => 'about')`. */ - public static $URL = ''; + public $route; /** - * @var \Codeception\AbstractGuy + * @var \Codeception\AbstractGuy the testing guy object */ protected $guy; + /** + * Constructor. + * @param \Codeception\AbstractGuy the testing guy object + */ public function __construct($I) { $this->guy = $I; } /** - * Basic route example for your current URL - * You can append any additional parameter to URL - * and use it in tests like: EditPage::route('/123-post'); + * Returns the URL to this page. + * The URL will be returned by calling the URL manager of the application + * with [[route]] and the provided parameters. + * @param array $params the GET parameters for creating the URL + * @return string the URL to this page + * @throws InvalidConfigException if [[route]] is not set or invalid */ - public static function route($param) + public function getUrl($params = []) { - return static::$URL.$param; + if (is_string($this->route)) { + return Yii::$app->getUrlManager()->createUrl($this->route, $params); + } elseif (is_array($this->route) && isset($this->route[0])) { + $route = $this->route[0]; + $ps = $this->route; + unset($this->route[0]); + return Yii::$app->getUrlManager()->createUrl($route, array_merge($ps, $params)); + } else { + throw new InvalidConfigException('The "route" property must be set.'); + } } /** - * @param $I - * @return static + * Creates a page instance and sets the test guy to use [[url]]. + * @param \Codeception\AbstractGuy $I the test guy instance + * @param array $params the GET parameters to be used to generate [[url]] + * @return static the page instance */ - public static function of($I) + public static function openBy($I, $params = []) { - return new static($I); + $page = new static($I); + $I->amOnPage($page->getUrl($params)); + return $page; } } From 3d9340032ee03f46440168fe00438b305cee0cbe Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 18 Dec 2013 00:40:52 -0500 Subject: [PATCH 30/77] test refactoring. --- apps/basic/commands/HelloController.php | 2 +- apps/basic/config/web.php | 6 ++---- apps/basic/tests/unit/_bootstrap.php | 4 ++-- apps/basic/web/index-test-acceptance.php | 5 ++--- apps/basic/web/index.php | 3 +-- extensions/yii/codeception/TestCase.php | 32 ++++++++++++++------------------ 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/apps/basic/commands/HelloController.php b/apps/basic/commands/HelloController.php index b62e5bb..ce567dd 100644 --- a/apps/basic/commands/HelloController.php +++ b/apps/basic/commands/HelloController.php @@ -25,6 +25,6 @@ class HelloController extends Controller */ public function actionIndex($message = 'hello world') { - echo $message."\n"; + echo $message . "\n"; } } diff --git a/apps/basic/config/web.php b/apps/basic/config/web.php index 966458d..472f842 100644 --- a/apps/basic/config/web.php +++ b/apps/basic/config/web.php @@ -39,16 +39,14 @@ $config = [ 'params' => $params, ]; -if (YII_ENV_DEV) -{ +if (YII_ENV_DEV) { // configuration adjustments for 'dev' environment $config['preload'][] = 'debug'; $config['modules']['debug'] = 'yii\debug\Module'; $config['modules']['gii'] = 'yii\gii\Module'; } -if (YII_ENV_TEST) -{ +if (YII_ENV_TEST) { // configuration adjustments for 'test' environment. // configuration for codeception test environments can be found in codeception folder. diff --git a/apps/basic/tests/unit/_bootstrap.php b/apps/basic/tests/unit/_bootstrap.php index d6d11e9..f37625b 100644 --- a/apps/basic/tests/unit/_bootstrap.php +++ b/apps/basic/tests/unit/_bootstrap.php @@ -2,7 +2,7 @@ // add unit testing specific bootstrap code here -yii\codeception\TestCase::$applicationConfig = yii\helpers\ArrayHelper::merge( +yii\codeception\TestCase::$appConfig = yii\helpers\ArrayHelper::merge( require(__DIR__ . '/../../config/web.php'), require(__DIR__ . '/../../config/codeception/unit.php') -); \ No newline at end of file +); diff --git a/apps/basic/web/index-test-acceptance.php b/apps/basic/web/index-test-acceptance.php index 224152b..ef9ec75 100644 --- a/apps/basic/web/index-test-acceptance.php +++ b/apps/basic/web/index-test-acceptance.php @@ -1,6 +1,6 @@ run(); +(new yii\web\Application($config))->run(); diff --git a/apps/basic/web/index.php b/apps/basic/web/index.php index e9eeb33..006e28f 100644 --- a/apps/basic/web/index.php +++ b/apps/basic/web/index.php @@ -9,5 +9,4 @@ require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); $config = require(__DIR__ . '/../config/web.php'); -$application = new yii\web\Application($config); -$application->run(); +(new yii\web\Application($config))->run(); diff --git a/extensions/yii/codeception/TestCase.php b/extensions/yii/codeception/TestCase.php index e6809bc..34dc11d 100644 --- a/extensions/yii/codeception/TestCase.php +++ b/extensions/yii/codeception/TestCase.php @@ -3,7 +3,6 @@ namespace yii\codeception; use Yii; -use yii\helpers\ArrayHelper; /** * TestCase is the base class for all codeception unit tests @@ -14,22 +13,18 @@ use yii\helpers\ArrayHelper; class TestCase extends \PHPUnit_Framework_TestCase { /** - * @var array|string Your application base config that will be used for creating application each time before test. - * This can be an array or alias, pointing to the config file. For example for console application it can be - * '@tests/unit/console_bootstrap.php' that can be similar to existing unit tests bootstrap file. + * @var array the application configuration that will be used for creating an application instance for each test. */ - public static $applicationConfig = '@app/config/web.php'; + public static $appConfig = []; /** - * @var array|string Your application config, will be merged with base config when creating application. Can be an alias too. + * @var string the application class that [[mockApplication()]] should use */ - protected $config = []; + public static $appClass = 'yii\web\Application'; + /** - * Created application class - * @var string + * @inheritdoc */ - protected $applicationClass = 'yii\web\Application'; - protected function tearDown() { $this->destroyApplication(); @@ -37,20 +32,21 @@ class TestCase extends \PHPUnit_Framework_TestCase } /** - * Sets up `Yii::$app`. + * Mocks up the application instance. + * @param array $config the configuration that should be used to generate the application instance. + * If null, [[appConfig]] will be used. + * @return \yii\web\Application|\yii\console\Application the application instance */ - protected function mockApplication() + protected function mockApplication($config = null) { - $baseConfig = is_array(static::$applicationConfig) ? static::$applicationConfig : require(Yii::getAlias(static::$applicationConfig)); - $config = is_array($this->config)? $this->config : require(Yii::getAlias($this->config)); - new $this->applicationClass(ArrayHelper::merge($baseConfig,$config)); + return new static::$appClass($config === null ? static::$appConfig : $config); } /** - * Destroys an application created via [[mockApplication]]. + * Destroys the application instance created by [[mockApplication]]. */ protected function destroyApplication() { - \Yii::$app = null; + Yii::$app = null; } } From 8c4412be5088e41ff6525b1cac21b8617679f66e Mon Sep 17 00:00:00 2001 From: Paul Kofmann Date: Wed, 18 Dec 2013 14:05:00 +0100 Subject: [PATCH 31/77] short array syntax --- framework/yii/db/oci/QueryBuilder.php | 2 +- framework/yii/db/oci/Schema.php | 2 +- framework/yii/test/DbFixtureManager.php | 2 +- framework/yii/web/AssetManager.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/yii/db/oci/QueryBuilder.php b/framework/yii/db/oci/QueryBuilder.php index cf618e0..cf8234f 100644 --- a/framework/yii/db/oci/QueryBuilder.php +++ b/framework/yii/db/oci/QueryBuilder.php @@ -45,7 +45,7 @@ class QueryBuilder extends \yii\db\QueryBuilder if (($limit < 0) && ($offset < 0)) { return $this->sql; } - $filters = array(); + $filters = []; if ($offset > 0) { $filters[] = 'rowNumId > ' . (int)$offset; } diff --git a/framework/yii/db/oci/Schema.php b/framework/yii/db/oci/Schema.php index 40ddd04..b9a51c2 100644 --- a/framework/yii/db/oci/Schema.php +++ b/framework/yii/db/oci/Schema.php @@ -221,7 +221,7 @@ EOD; } $rows = $command->queryAll(); - $names = array(); + $names = []; foreach ($rows as $row) { $names[] = $row['TABLE_NAME']; } diff --git a/framework/yii/test/DbFixtureManager.php b/framework/yii/test/DbFixtureManager.php index 57a4f4a..d78d28f 100644 --- a/framework/yii/test/DbFixtureManager.php +++ b/framework/yii/test/DbFixtureManager.php @@ -52,7 +52,7 @@ class DbFixtureManager extends Component public $db = 'db'; /** * @var array list of database schemas that the test tables may reside in. Defaults to - * array(''), meaning using the default schema (an empty string refers to the + * [''], meaning using the default schema (an empty string refers to the * default schema). This property is mainly used when turning on and off integrity checks * so that fixture data can be populated into the database without causing problem. */ diff --git a/framework/yii/web/AssetManager.php b/framework/yii/web/AssetManager.php index 0a8b0b8..89e746e 100644 --- a/framework/yii/web/AssetManager.php +++ b/framework/yii/web/AssetManager.php @@ -131,7 +131,7 @@ class AssetManager extends Component if ($this->bundles[$name] instanceof AssetBundle) { return $this->bundles[$name]; } elseif (is_array($this->bundles[$name])) { - $bundle = Yii::createObject(array_merge(array('class' => $name), $this->bundles[$name])); + $bundle = Yii::createObject(array_merge(['class' => $name], $this->bundles[$name])); } else { throw new InvalidConfigException("Invalid asset bundle: $name"); } From 060775b0dabf3b2a27aa35a4135dde61a800962f Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 14:31:43 +0100 Subject: [PATCH 32/77] Json::encode did not handle JsonSerializable objects --- framework/CHANGELOG.md | 1 + framework/yii/helpers/BaseJson.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1c36d5d..cc0d4b0 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -11,6 +11,7 @@ Yii Framework 2 Change Log - Bug #1545: It was not possible to execute db Query twice, params where missing (cebe) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) +- Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) - Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) diff --git a/framework/yii/helpers/BaseJson.php b/framework/yii/helpers/BaseJson.php index 35b35ff..019c7ef 100644 --- a/framework/yii/helpers/BaseJson.php +++ b/framework/yii/helpers/BaseJson.php @@ -81,6 +81,10 @@ class BaseJson */ protected static function processData($data, &$expressions, $expPrefix) { + if ($data instanceof \JsonSerializable) { + return $data; + } + if (is_object($data)) { if ($data instanceof JsExpression) { $token = "!{[$expPrefix=" . count($expressions) . ']}!'; From 7b8289275f83421a45d35596a0fdf44bf8c91543 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 18 Dec 2013 16:15:21 +0100 Subject: [PATCH 33/77] Quick example of inline validator (original doc did not mention inline validator) --- docs/guide/validation.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 431300f..1b1e92e 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -110,6 +110,19 @@ Validates that the attribute value is among a list of values. - `strict` whether the comparison is strict (both type and value must be the same). _(false)_ - `not` whether to invert the validation logic. _(false)_ +### `inline`: [[InlineValidator]] + +Uses a custom function to validate the attribute. You need to define a public method in your model class which will evaluate the validity of the attribute. For example, if an attribute needs to be divisible by 10. In the rules you would define: `['attributeName', 'myValidationMethod'],`. + +Then, your own method could look like this: +```php +public function myValidationMethod($attribute) { + if(($attribute % 10) != 0) { + $this->addError($attribute, 'cannot divide value by 10'); + } +} +``` + ### `integer`: [[NumberValidator]] Validates that the attribute value is an integer number. From 37e22e577ed3251a890b0f48dfe075e9e6709acd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 16:33:20 +0100 Subject: [PATCH 34/77] Paginiation and Sort can now create absolute URLs --- framework/CHANGELOG.md | 1 + framework/yii/data/Pagination.php | 15 +++++++++++++-- framework/yii/data/Sort.php | 9 +++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index cc0d4b0..e7e62d9 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -21,6 +21,7 @@ Yii Framework 2 Change Log - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) +- Enh: Sort and Paginiation can now create absolute URLs (cebe) - Chg: Renamed yii\jui\Widget::clientEventsMap to clientEventMap (qiangxue) - New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo) diff --git a/framework/yii/data/Pagination.php b/framework/yii/data/Pagination.php index 6a85dd2..8fa8b87 100644 --- a/framework/yii/data/Pagination.php +++ b/framework/yii/data/Pagination.php @@ -91,6 +91,11 @@ class Pagination extends Object */ public $params; /** + * @var \yii\web\UrlManager the URL manager used for creating pagination URLs. If not set, + * the "urlManager" application component will be used. + */ + public $urlManager; + /** * @var boolean whether to check if [[page]] is within valid range. * When this property is true, the value of [[page]] will always be between 0 and ([[pageCount]]-1). * Because [[pageCount]] relies on the correct value of [[totalCount]] which may not be available @@ -167,11 +172,12 @@ class Pagination extends Object * Creates the URL suitable for pagination with the specified page number. * This method is mainly called by pagers when creating URLs used to perform pagination. * @param integer $page the zero-based page number that the URL should point to. + * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. * @return string the created URL * @see params * @see forcePageVar */ - public function createUrl($page) + public function createUrl($page, $absolute = false) { if (($params = $this->params) === null) { $request = Yii::$app->getRequest(); @@ -183,7 +189,12 @@ class Pagination extends Object unset($params[$this->pageVar]); } $route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; - return Yii::$app->getUrlManager()->createUrl($route, $params); + $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; + if ($absolute) { + return $urlManager->createAbsoluteUrl($route, $params); + } else { + return $urlManager->createUrl($route, $params); + } } /** diff --git a/framework/yii/data/Sort.php b/framework/yii/data/Sort.php index 8a1b36c..0432006 100644 --- a/framework/yii/data/Sort.php +++ b/framework/yii/data/Sort.php @@ -329,12 +329,13 @@ class Sort extends Object * For example, if the current page already sorts the data by the specified attribute in ascending order, * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. * @param string $attribute the attribute name + * @param boolean $absolute whether to create an absolute URL. Defaults to `false`. * @return string the URL for sorting. False if the attribute is invalid. * @throws InvalidConfigException if the attribute is unknown * @see attributeOrders * @see params */ - public function createUrl($attribute) + public function createUrl($attribute, $absolute = false) { if (($params = $this->params) === null) { $request = Yii::$app->getRequest(); @@ -343,7 +344,11 @@ class Sort extends Object $params[$this->sortVar] = $this->createSortVar($attribute); $route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; - return $urlManager->createUrl($route, $params); + if ($absolute) { + return $urlManager->createAbsoluteUrl($route, $params); + } else { + return $urlManager->createUrl($route, $params); + } } /** From 3181964132a132ff56cca74663029848fb74c243 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 18 Dec 2013 11:12:04 -0500 Subject: [PATCH 35/77] refactored TestCase. --- extensions/yii/codeception/TestCase.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/extensions/yii/codeception/TestCase.php b/extensions/yii/codeception/TestCase.php index 34dc11d..e30aa1d 100644 --- a/extensions/yii/codeception/TestCase.php +++ b/extensions/yii/codeception/TestCase.php @@ -3,6 +3,7 @@ namespace yii\codeception; use Yii; +use yii\base\InvalidConfigException; /** * TestCase is the base class for all codeception unit tests @@ -13,7 +14,8 @@ use Yii; class TestCase extends \PHPUnit_Framework_TestCase { /** - * @var array the application configuration that will be used for creating an application instance for each test. + * @var array|string the application configuration that will be used for creating an application instance for each test. + * You can use a string to represent the file path or path alias of a configuration file. */ public static $appConfig = []; /** @@ -21,6 +23,14 @@ class TestCase extends \PHPUnit_Framework_TestCase */ public static $appClass = 'yii\web\Application'; + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } /** * @inheritdoc @@ -39,7 +49,14 @@ class TestCase extends \PHPUnit_Framework_TestCase */ protected function mockApplication($config = null) { - return new static::$appClass($config === null ? static::$appConfig : $config); + $config = $config === null ? static::$appConfig : $config; + if (is_string($config)) { + $config = Yii::getAlias($config); + } + if (!is_array($config)) { + throw new InvalidConfigException('Please provide a configuration for creating application.'); + } + return new static::$appClass($config); } /** From b4612637ec660ab47f1f4e208dfc10b369cd318a Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 17:22:04 +0100 Subject: [PATCH 36/77] make count behave like in SQL also fixed count behavior according to limit and offset. fixes #1458 --- extensions/yii/elasticsearch/Query.php | 8 +------- extensions/yii/redis/ActiveQuery.php | 12 +++++++++--- framework/yii/db/Query.php | 15 ++++++++++++++- tests/unit/extensions/redis/ActiveRecordTest.php | 8 -------- tests/unit/framework/ar/ActiveRecordTestTrait.php | 19 +++++++++++++++---- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/extensions/yii/elasticsearch/Query.php b/extensions/yii/elasticsearch/Query.php index 79e58c3..f404fc1 100644 --- a/extensions/yii/elasticsearch/Query.php +++ b/extensions/yii/elasticsearch/Query.php @@ -283,13 +283,7 @@ class Query extends Component implements QueryInterface $options = []; $options['search_type'] = 'count'; - $count = $this->createCommand($db)->search($options)['hits']['total']; - if ($this->limit === null && $this->offset === null) { - return $count; - } elseif ($this->offset !== null) { - $count = $this->offset < $count ? $count - $this->offset : 0; - } - return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); + return $this->createCommand($db)->search($options)['hits']['total']; } /** diff --git a/extensions/yii/redis/ActiveQuery.php b/extensions/yii/redis/ActiveQuery.php index 607b18e..b64beba 100644 --- a/extensions/yii/redis/ActiveQuery.php +++ b/extensions/yii/redis/ActiveQuery.php @@ -126,7 +126,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface */ public function count($q = '*', $db = null) { - if ($this->offset === null && $this->limit === null && $this->where === null) { + if ($this->where === null) { /** @var ActiveRecord $modelClass */ $modelClass = $this->modelClass; if ($db === null) { @@ -291,11 +291,17 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface /** @var ActiveRecord $modelClass */ $modelClass = $this->modelClass; - $start = $this->offset === null ? 0 : $this->offset; + if ($type == 'Count') { + $start = 0; + $limit = null; + } else { + $start = $this->offset === null ? 0 : $this->offset; + $limit = $this->limit; + } $i = 0; $data = []; foreach($pks as $pk) { - if (++$i > $start && ($this->limit === null || $i <= $start + $this->limit)) { + if (++$i > $start && ($limit === null || $i <= $start + $limit)) { $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk); $result = $db->executeCommand('HGETALL', [$key]); if (!empty($result)) { diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 0d81887..ee24c2f 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -253,7 +253,11 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { - return $this->queryScalar(new Expression('1'), $db) !== false; + $select = $this->select; + $this->select = [new Expression('1')]; + $command = $this->createCommand($db); + $this->select = $select; + return $command->queryScalar() !== false; } /** @@ -266,9 +270,18 @@ class Query extends Component implements QueryInterface private function queryScalar($selectExpression, $db) { $select = $this->select; + $limit = $this->limit; + $offset = $this->offset; + $this->select = [$selectExpression]; + $this->limit = null; + $this->offset = null; $command = $this->createCommand($db); + $this->select = $select; + $this->limit = $limit; + $this->offset = $offset; + return $command->queryScalar(); } diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php index f3cbbdc..3916f9d 100644 --- a/tests/unit/extensions/redis/ActiveRecordTest.php +++ b/tests/unit/extensions/redis/ActiveRecordTest.php @@ -205,14 +205,6 @@ class ActiveRecordTest extends RedisTestCase $this->assertEquals(2, $order->items[1]->id); } - public function testFindCount() - { - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(1, Customer::find()->limit(1)->count()); - $this->assertEquals(2, Customer::find()->limit(2)->count()); - $this->assertEquals(1, Customer::find()->offset(2)->limit(2)->count()); - } - public function testFindColumn() { $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index 3f1739e..50a5f81 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -287,10 +287,17 @@ trait ActiveRecordTestTrait { /** @var TestCase|ActiveRecordTestTrait $this */ $this->assertEquals(3, $this->callCustomerFind()->count()); - // TODO should limit have effect on count() -// $this->assertEquals(1, $this->callCustomerFind()->limit(1)->count()); -// $this->assertEquals(2, $this->callCustomerFind()->limit(2)->count()); -// $this->assertEquals(1, $this->callCustomerFind()->offset(2)->limit(2)->count()); + + $this->assertEquals(1, $this->callCustomerFind()->where(['id' => 1])->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(1)->count()); + $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->offset(2)->count()); + + // limit should have no effect on count() + $this->assertEquals(3, $this->callCustomerFind()->limit(1)->count()); + $this->assertEquals(3, $this->callCustomerFind()->limit(2)->count()); + $this->assertEquals(3, $this->callCustomerFind()->limit(10)->count()); + $this->assertEquals(3, $this->callCustomerFind()->offset(2)->limit(2)->count()); } public function testFindLimit() @@ -371,6 +378,10 @@ trait ActiveRecordTestTrait $this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists()); $this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists()); $this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->exists()); + + $this->assertTrue($this->callCustomerFind()->where(['id' => [2,3]])->exists()); + $this->assertTrue($this->callCustomerFind()->where(['id' => [2,3]])->offset(1)->exists()); + $this->assertFalse($this->callCustomerFind()->where(['id' => [2,3]])->offset(2)->exists()); } public function testFindLazy() From 702cf513ca850ad55ce59a19dbbb4686e9ffe44c Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 18 Dec 2013 17:31:32 +0100 Subject: [PATCH 37/77] make elasticsearch tests only work on yiitext index --- tests/unit/extensions/elasticsearch/QueryTest.php | 27 +++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/unit/extensions/elasticsearch/QueryTest.php b/tests/unit/extensions/elasticsearch/QueryTest.php index da2558e..746f9a2 100644 --- a/tests/unit/extensions/elasticsearch/QueryTest.php +++ b/tests/unit/extensions/elasticsearch/QueryTest.php @@ -15,12 +15,15 @@ class QueryTest extends ElasticSearchTestCase $command = $this->getConnection()->createCommand(); - $command->deleteAllIndexes(); + // delete index + if ($command->indexExists('yiitest')) { + $command->deleteIndex('yiitest'); + } - $command->insert('test', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); - $command->insert('test', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); - $command->insert('test', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); - $command->insert('test', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); + $command->insert('yiitest', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); + $command->insert('yiitest', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); + $command->insert('yiitest', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); + $command->insert('yiitest', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); $command->flushIndex(); } @@ -28,7 +31,7 @@ class QueryTest extends ElasticSearchTestCase public function testFields() { $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $query->fields(['name', 'status']); $this->assertEquals(['name', 'status'], $query->fields); @@ -63,7 +66,7 @@ class QueryTest extends ElasticSearchTestCase public function testOne() { $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $result = $query->one($this->getConnection()); $this->assertEquals(3, count($result['_source'])); @@ -87,7 +90,7 @@ class QueryTest extends ElasticSearchTestCase public function testAll() { $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $results = $query->all($this->getConnection()); $this->assertEquals(4, count($results)); @@ -99,7 +102,7 @@ class QueryTest extends ElasticSearchTestCase $this->assertArrayHasKey('_id', $result); $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $results = $query->where(['name' => 'user1'])->all($this->getConnection()); $this->assertEquals(1, count($results)); @@ -113,7 +116,7 @@ class QueryTest extends ElasticSearchTestCase // indexBy $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $results = $query->indexBy('name')->all($this->getConnection()); $this->assertEquals(4, count($results)); @@ -124,7 +127,7 @@ class QueryTest extends ElasticSearchTestCase public function testScalar() { $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); $this->assertEquals('user1', $result); @@ -137,7 +140,7 @@ class QueryTest extends ElasticSearchTestCase public function testColumn() { $query = new Query; - $query->from('test', 'user'); + $query->from('yiitest', 'user'); $result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection()); $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); From 226c9f22d05b995def3f8ad98b378e405667eac4 Mon Sep 17 00:00:00 2001 From: Jacob Morrison Date: Wed, 18 Dec 2013 08:56:19 -0800 Subject: [PATCH 38/77] Fixed issue with tabular input in ActiveField::radio and checkbox --- framework/CHANGELOG.md | 1 + framework/yii/widgets/ActiveField.php | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e7e62d9..073a3ce 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -12,6 +12,7 @@ Yii Framework 2 Change Log - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) +- Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom) - Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php index 0320516..4228ea9 100644 --- a/framework/yii/widgets/ActiveField.php +++ b/framework/yii/widgets/ActiveField.php @@ -371,7 +371,8 @@ class ActiveField extends Component { if ($enclosedByLabel) { if (!isset($options['label'])) { - $options['label'] = Html::encode($this->model->getAttributeLabel($this->attribute)); + $attribute = Html::getAttributeName($this->attribute); + $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); } $this->parts['{input}'] = Html::activeRadio($this->model, $this->attribute, $options); $this->parts['{label}'] = ''; @@ -406,7 +407,8 @@ class ActiveField extends Component { if ($enclosedByLabel) { if (!isset($options['label'])) { - $options['label'] = Html::encode($this->model->getAttributeLabel($this->attribute)); + $attribute = Html::getAttributeName($this->attribute); + $options['label'] = Html::encode($this->model->getAttributeLabel($attribute)); } $this->parts['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options); $this->parts['{label}'] = ''; From 01c0dd3c4cb5f808abc6892e2b9cddd89fe626c2 Mon Sep 17 00:00:00 2001 From: Luciano Baraglia Date: Thu, 19 Dec 2013 00:13:29 -0300 Subject: [PATCH 39/77] Debug tables wraps content [SKIP CI] --- extensions/yii/debug/assets/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/debug/assets/main.css b/extensions/yii/debug/assets/main.css index 7953873..5987af1 100644 --- a/extensions/yii/debug/assets/main.css +++ b/extensions/yii/debug/assets/main.css @@ -148,6 +148,6 @@ ul.trace { } td, th { - white-space: pre; + white-space: pre-line; word-wrap: break-word; } From 21fff9429c7ae757a44aeeb2b09d3b36cda8b017 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 19 Dec 2013 12:21:20 +0200 Subject: [PATCH 40/77] Mongo full text search test updated. --- tests/unit/extensions/mongodb/CollectionTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/extensions/mongodb/CollectionTest.php b/tests/unit/extensions/mongodb/CollectionTest.php index 24ffc6c..fb6238d 100644 --- a/tests/unit/extensions/mongodb/CollectionTest.php +++ b/tests/unit/extensions/mongodb/CollectionTest.php @@ -303,11 +303,17 @@ class CollectionTest extends MongoDbTestCase 'status' => 1, 'amount' => 200, ], + [ + 'name' => 'no search keyword', + 'status' => 1, + 'amount' => 200, + ], ]; $collection->batchInsert($rows); $collection->createIndex(['name' => 'text']); - $result = $collection->fullTextSearch('some'); + $result = $collection->fullTextSearch('customer'); $this->assertNotEmpty($result); + $this->assertCount(2, $result); } } \ No newline at end of file From d8cf758187a1cd231065ca9cee0401810246e0ac Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 19 Dec 2013 13:21:26 +0100 Subject: [PATCH 41/77] Update controller.md Fixes #1577 --- docs/guide/controller.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/controller.md b/docs/guide/controller.md index 584c738..c07968b 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -144,7 +144,7 @@ If action is generic enough it makes sense to implement it in a separate class t Create `actions/Page.php` ```php -namespace \app\actions; +namespace app\actions; class Page extends \yii\base\Action { @@ -152,7 +152,7 @@ class Page extends \yii\base\Action public function run() { - $this->controller->render($view); + return $this->controller->render($view); } } ``` From 19c7c001d4767c03ca67a7102b2b39e9e08c6793 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 19 Dec 2013 13:24:38 +0100 Subject: [PATCH 42/77] Moved elasticsearch connect to a later pos in travis.yml avoid failure when it is not started at that point --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index da19dcb..def1c47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,13 +14,13 @@ services: install: - composer self-update && composer --version # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist - - echo 'elasticsearch version ' && curl http://localhost:9200/ - tests/unit/data/travis/mongodb-setup.sh - tests/unit/data/travis/apc-setup.sh - tests/unit/data/travis/memcache-setup.sh - tests/unit/data/travis/cubrid-setup.sh before_script: + - echo 'elasticsearch version ' && curl http://localhost:9200/ - mysql -e 'CREATE DATABASE yiitest;'; - psql -U postgres -c 'CREATE DATABASE yiitest;'; - tests/unit/data/travis/sphinx-setup.sh From 65d72eb75b844792ab6735c19fca397e4dc8f54e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 19 Dec 2013 11:12:51 -0500 Subject: [PATCH 43/77] updated doc about using scopes. --- docs/guide/active-record.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 601dfb5..70d41e0 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -450,7 +450,7 @@ in the ActiveRecord classes. They can be invoked through the [[ActiveQuery]] obj via [[find()]] or [[findBySql()]]. The following is an example: ```php -class Customer extends \yii\db\ActiveRecord +class Comment extends \yii\db\ActiveRecord { // ... @@ -463,11 +463,34 @@ class Customer extends \yii\db\ActiveRecord } } -$customers = Customer::find()->active()->all(); +$comments = Comment::find()->active()->all(); +``` + +In the above, the `active()` method is defined in `Comment` while we are calling it +through `ActiveQuery` returned by `Comment::find()`. + +You can also use scopes when defining relations. For example, + +```php +class Post extends \yii\db\ActiveRecord +{ + public function getComments() + { + return $this->hasMany(Comment::className(), ['post_id' => 'id'])->active(); + + } +} ``` -In the above, the `active()` method is defined in `Customer` while we are calling it -through `ActiveQuery` returned by `Customer::find()`. +Or use the scopes on-the-fly when performing relational query: + +```php +$posts = Post::find()->with([ + 'comments' => function($q) { + $q->active(); + } +])->all(); +``` Scopes can be parameterized. For example, we can define and use the following `olderThan` scope: From a53b3e577e9549d6f4a723852df3d1fd3d283433 Mon Sep 17 00:00:00 2001 From: tonydspaniard Date: Fri, 20 Dec 2013 12:48:38 +0100 Subject: [PATCH 44/77] Update collapse to use bootstrap 3 classes fixes #1459 --- extensions/yii/bootstrap/Collapse.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/extensions/yii/bootstrap/Collapse.php b/extensions/yii/bootstrap/Collapse.php index 08bde22..d77d15e 100644 --- a/extensions/yii/bootstrap/Collapse.php +++ b/extensions/yii/bootstrap/Collapse.php @@ -66,7 +66,7 @@ class Collapse extends Widget public function init() { parent::init(); - Html::addCssClass($this->options, 'accordion'); + Html::addCssClass($this->options, 'panel-group'); } /** @@ -90,7 +90,7 @@ class Collapse extends Widget $index = 0; foreach ($this->items as $header => $item) { $options = ArrayHelper::getValue($item, 'options', []); - Html::addCssClass($options, 'accordion-group'); + Html::addCssClass($options, 'panel panel-default'); $items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options); } @@ -111,21 +111,23 @@ class Collapse extends Widget $id = $this->options['id'] . '-collapse' . $index; $options = ArrayHelper::getValue($item, 'contentOptions', []); $options['id'] = $id; - Html::addCssClass($options, 'accordion-body collapse'); + Html::addCssClass($options, 'panel-collapse collapse'); - $header = Html::a($header, '#' . $id, [ + $headerToggle = Html::a($header, '#' . $id, [ 'class' => 'accordion-toggle', 'data-toggle' => 'collapse', 'data-parent' => '#' . $this->options['id'] ]) . "\n"; - $content = Html::tag('div', $item['content'], ['class' => 'accordion-inner']) . "\n"; + $header = Html::tag('h4', $headerToggle, ['class' => 'panel-title']); + + $content = Html::tag('div', $item['content'], ['class' => 'panel-body']) . "\n"; } else { throw new InvalidConfigException('The "content" option is required.'); } $group = []; - $group[] = Html::tag('div', $header, ['class' => 'accordion-heading']); + $group[] = Html::tag('div', $header, ['class' => 'panel-heading']); $group[] = Html::tag('div', $content, $options); return implode("\n", $group); From 92a967c35cd4ef638f65b0bf47d619f1d03f17f3 Mon Sep 17 00:00:00 2001 From: tonydspaniard Date: Fri, 20 Dec 2013 12:49:48 +0100 Subject: [PATCH 45/77] Modify class name to a more logical one --- extensions/yii/bootstrap/Collapse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/bootstrap/Collapse.php b/extensions/yii/bootstrap/Collapse.php index d77d15e..794c5e8 100644 --- a/extensions/yii/bootstrap/Collapse.php +++ b/extensions/yii/bootstrap/Collapse.php @@ -114,7 +114,7 @@ class Collapse extends Widget Html::addCssClass($options, 'panel-collapse collapse'); $headerToggle = Html::a($header, '#' . $id, [ - 'class' => 'accordion-toggle', + 'class' => 'collapse-toggle', 'data-toggle' => 'collapse', 'data-parent' => '#' . $this->options['id'] ]) . "\n"; From 47fdd47f96413d95340108a91281428b950cc1f1 Mon Sep 17 00:00:00 2001 From: tonydspaniard Date: Fri, 20 Dec 2013 13:19:35 +0100 Subject: [PATCH 46/77] Update changelog --- extensions/yii/bootstrap/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/yii/bootstrap/CHANGELOG.md b/extensions/yii/bootstrap/CHANGELOG.md index 94b3d60..0eaa722 100644 --- a/extensions/yii/bootstrap/CHANGELOG.md +++ b/extensions/yii/bootstrap/CHANGELOG.md @@ -6,6 +6,7 @@ Yii Framework 2 bootstrap extension Change Log - Enh #1474: Added option to make NavBar 100% width (cebe) - Enh #1553: Only add navbar-default class to NavBar when no other class is specified (cebe) +- Bug #1459: Update Collapse to use bootstrap 3 classes (tonydspaniard) 2.0.0 alpha, December 1, 2013 ----------------------------- From 594fd2daed88ad939acf727467e8a4f4d3511f95 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 20 Dec 2013 07:30:23 -0500 Subject: [PATCH 47/77] fixed composer.json. --- extensions/yii/bootstrap/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/bootstrap/composer.json b/extensions/yii/bootstrap/composer.json index 32e9681..3e6031e 100644 --- a/extensions/yii/bootstrap/composer.json +++ b/extensions/yii/bootstrap/composer.json @@ -19,7 +19,7 @@ ], "require": { "yiisoft/yii2": "*", - "twbs/bootstrap": "v3.0.*" + "twbs/bootstrap": "3.0.*" }, "autoload": { "psr-0": { "yii\\bootstrap\\": "" } From a126419e9e0b1046190fa3fffdaf18cb81fb09ca Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 21 Dec 2013 15:37:49 -0500 Subject: [PATCH 48/77] Fixes #1591: StringValidator is accessing undefined property --- framework/CHANGELOG.md | 1 + framework/yii/validators/StringValidator.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 073a3ce..a1d7bdf 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue) - Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue) - Bug #1545: It was not possible to execute db Query twice, params where missing (cebe) +- Bug #1591: StringValidator is accessing undefined property (qiangxue) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) diff --git a/framework/yii/validators/StringValidator.php b/framework/yii/validators/StringValidator.php index a93fb72..dbc4001 100644 --- a/framework/yii/validators/StringValidator.php +++ b/framework/yii/validators/StringValidator.php @@ -174,7 +174,7 @@ class StringValidator extends Validator $options['is'] = $this->length; $options['notEqual'] = Html::encode(strtr($this->notEqual, [ '{attribute}' => $label, - '{length}' => $this->is, + '{length}' => $this->length, ])); } if ($this->skipOnEmpty) { From 0ff8518c2103a83587a53e44dd3d82e5d58c5a65 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 21 Dec 2013 17:07:31 -0500 Subject: [PATCH 49/77] Fixes #1550: fixed the issue that JUI input widgets did not property input IDs. --- extensions/yii/jui/CHANGELOG.md | 2 +- extensions/yii/jui/DatePicker.php | 23 +++++++++++++++++++---- extensions/yii/jui/InputWidget.php | 6 +++++- extensions/yii/jui/SliderInput.php | 30 +++++++++++++++++++++--------- extensions/yii/jui/Widget.php | 18 +++++++++++------- framework/CHANGELOG.md | 4 +++- framework/yii/captcha/Captcha.php | 7 ------- framework/yii/widgets/InputWidget.php | 10 +++++++++- framework/yii/widgets/MaskedInput.php | 8 -------- 9 files changed, 69 insertions(+), 39 deletions(-) diff --git a/extensions/yii/jui/CHANGELOG.md b/extensions/yii/jui/CHANGELOG.md index eb30e09..b31c34e 100644 --- a/extensions/yii/jui/CHANGELOG.md +++ b/extensions/yii/jui/CHANGELOG.md @@ -4,7 +4,7 @@ Yii Framework 2 jui extension Change Log 2.0.0 beta under development ---------------------------- -- no changes in this release. +- Bug #1550: fixed the issue that JUI input widgets did not property input IDs. 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/yii/jui/DatePicker.php b/extensions/yii/jui/DatePicker.php index 06ca356..bab2abe 100644 --- a/extensions/yii/jui/DatePicker.php +++ b/extensions/yii/jui/DatePicker.php @@ -54,14 +54,30 @@ class DatePicker extends InputWidget * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field. */ public $inline = false; + /** + * @var array the HTML attributes for the container tag. This is only used when [[inline]] is true. + */ + public $containerOptions = []; /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->inline && !isset($this->containerOptions['id'])) { + $this->containerOptions['id'] = $this->options['id'] . '-container'; + } + } + + /** * Renders the widget. */ public function run() { echo $this->renderWidget() . "\n"; + $containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id']; if ($this->language !== false) { $view = $this->getView(); DatePickerRegionalAsset::register($view); @@ -71,10 +87,10 @@ class DatePicker extends InputWidget $options = $this->clientOptions; $this->clientOptions = false; // the datepicker js widget is already registered - $this->registerWidget('datepicker', DatePickerAsset::className()); + $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); $this->clientOptions = $options; } else { - $this->registerWidget('datepicker', DatePickerAsset::className()); + $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID); } } @@ -101,8 +117,7 @@ class DatePicker extends InputWidget $this->clientOptions['defaultDate'] = $this->value; } $this->clientOptions['altField'] = '#' . $this->options['id']; - $this->options['id'] .= '-container'; - $contents[] = Html::tag('div', null, $this->options); + $contents[] = Html::tag('div', null, $this->containerOptions); } return implode("\n", $contents); diff --git a/extensions/yii/jui/InputWidget.php b/extensions/yii/jui/InputWidget.php index e100d6c..0facfb9 100644 --- a/extensions/yii/jui/InputWidget.php +++ b/extensions/yii/jui/InputWidget.php @@ -10,6 +10,7 @@ namespace yii\jui; use Yii; use yii\base\Model; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * InputWidget is the base class for all jQuery UI input widgets. @@ -44,7 +45,10 @@ class InputWidget extends Widget public function init() { if (!$this->hasModel() && $this->name === null) { - throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified."); + throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); + } + if ($this->hasModel() && !isset($this->options['id'])) { + $this->options['id'] = Html::getInputId($this->model, $this->attribute); } parent::init(); } diff --git a/extensions/yii/jui/SliderInput.php b/extensions/yii/jui/SliderInput.php index 8ded4e8..8a43eb1 100644 --- a/extensions/yii/jui/SliderInput.php +++ b/extensions/yii/jui/SliderInput.php @@ -50,30 +50,42 @@ class SliderInput extends InputWidget 'start' => 'slidestart', 'stop' => 'slidestop', ]; + /** + * @var array the HTML attributes for the container tag. + */ + public $containerOptions = []; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if (!isset($this->containerOptions['id'])) { + $this->containerOptions['id'] = $this->options['id'] . '-container'; + } + } /** * Executes the widget. */ public function run() { - echo Html::tag('div', '', $this->options); + echo Html::tag('div', '', $this->containerOptions); - $inputId = $this->id.'-input'; - $inputOptions = $this->options; - $inputOptions['id'] = $inputId; if ($this->hasModel()) { - echo Html::activeHiddenInput($this->model, $this->attribute, $inputOptions); + echo Html::activeHiddenInput($this->model, $this->attribute, $this->options); } else { - echo Html::hiddenInput($this->name, $this->value, $inputOptions); + echo Html::hiddenInput($this->name, $this->value, $this->options); } if (!isset($this->clientEvents['slide'])) { $this->clientEvents['slide'] = 'function(event, ui) { - $("#'.$inputId.'").val(ui.value); + $("#' . $this->options['id'] . '").val(ui.value); }'; } - $this->registerWidget('slider', SliderAsset::className()); - $this->getView()->registerJs('$("#'.$inputId.'").val($("#'.$this->id.'").slider("value"));'); + $this->registerWidget('slider', SliderAsset::className(), $this->containerOptions['id']); + $this->getView()->registerJs('$("#' . $this->options['id'] . '").val($("#' . $this->id . '").slider("value"));'); } } diff --git a/extensions/yii/jui/Widget.php b/extensions/yii/jui/Widget.php index 90bad68..8881a77 100644 --- a/extensions/yii/jui/Widget.php +++ b/extensions/yii/jui/Widget.php @@ -76,11 +76,11 @@ class Widget extends \yii\base\Widget /** * Registers a specific jQuery UI widget options * @param string $name the name of the jQuery UI widget + * @param string $id the ID of the widget */ - protected function registerClientOptions($name) + protected function registerClientOptions($name, $id) { if ($this->clientOptions !== false) { - $id = $this->options['id']; $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); $js = "jQuery('#$id').$name($options);"; $this->getView()->registerJs($js); @@ -90,11 +90,11 @@ class Widget extends \yii\base\Widget /** * Registers a specific jQuery UI widget events * @param string $name the name of the jQuery UI widget + * @param string $id the ID of the widget */ - protected function registerClientEvents($name) + protected function registerClientEvents($name, $id) { if (!empty($this->clientEvents)) { - $id = $this->options['id']; $js = []; foreach ($this->clientEvents as $event => $handler) { if (isset($this->clientEventMap[$event])) { @@ -112,11 +112,15 @@ class Widget extends \yii\base\Widget * Registers a specific jQuery UI widget asset bundle, initializes it with client options and registers related events * @param string $name the name of the jQuery UI widget * @param string $assetBundle the asset bundle for the widget + * @param string $id the ID of the widget. If null, it will use the `id` value of [[options]]. */ - protected function registerWidget($name, $assetBundle) + protected function registerWidget($name, $assetBundle, $id = null) { + if ($id === null) { + $id = $this->options['id']; + } $this->registerAssets($assetBundle); - $this->registerClientOptions($name); - $this->registerClientEvents($name); + $this->registerClientOptions($name, $id); + $this->registerClientEvents($name, $id); } } diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index a1d7bdf..9694cb7 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue) - Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue) - Bug #1545: It was not possible to execute db Query twice, params where missing (cebe) +- Bug #1550: fixed the issue that JUI input widgets did not property input IDs. - Bug #1591: StringValidator is accessing undefined property (qiangxue) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) @@ -24,7 +25,8 @@ Yii Framework 2 Change Log - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Sort and Paginiation can now create absolute URLs (cebe) -- Chg: Renamed yii\jui\Widget::clientEventsMap to clientEventMap (qiangxue) +- Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) +- Chg: Added `yii\widgets\InputWidget::options` (qiangxue) - New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo) diff --git a/framework/yii/captcha/Captcha.php b/framework/yii/captcha/Captcha.php index 76090a2..18b8765 100644 --- a/framework/yii/captcha/Captcha.php +++ b/framework/yii/captcha/Captcha.php @@ -39,10 +39,6 @@ class Captcha extends InputWidget */ public $captchaAction = 'site/captcha'; /** - * @var array HTML attributes to be applied to the text input field. - */ - public $options = []; - /** * @var array HTML attributes to be applied to the CAPTCHA image tag. */ public $imageOptions = []; @@ -62,9 +58,6 @@ class Captcha extends InputWidget $this->checkRequirements(); - if (!isset($this->options['id'])) { - $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); - } if (!isset($this->imageOptions['id'])) { $this->imageOptions['id'] = $this->options['id'] . '-image'; } diff --git a/framework/yii/widgets/InputWidget.php b/framework/yii/widgets/InputWidget.php index e1981c9..0a4b5b7 100644 --- a/framework/yii/widgets/InputWidget.php +++ b/framework/yii/widgets/InputWidget.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Widget; use yii\base\Model; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * InputWidget is the base class for widgets that collect user inputs. @@ -40,6 +41,10 @@ class InputWidget extends Widget * @var string the input value. */ public $value; + /** + * @var array the HTML attributes for the input tag. + */ + public $options = []; /** @@ -49,7 +54,10 @@ class InputWidget extends Widget public function init() { if (!$this->hasModel() && $this->name === null) { - throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified."); + throw new InvalidConfigException("Either 'name', or 'model' and 'attribute' properties must be specified."); + } + if (!isset($this->options['id'])) { + $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); } parent::init(); } diff --git a/framework/yii/widgets/MaskedInput.php b/framework/yii/widgets/MaskedInput.php index fc21cef..7eb42a7 100644 --- a/framework/yii/widgets/MaskedInput.php +++ b/framework/yii/widgets/MaskedInput.php @@ -61,10 +61,6 @@ class MaskedInput extends InputWidget * @var string a JavaScript function callback that will be invoked when user finishes the input. */ public $completed; - /** - * @var array the HTML attributes for the input tag. - */ - public $options = []; /** @@ -77,10 +73,6 @@ class MaskedInput extends InputWidget if (empty($this->mask)) { throw new InvalidConfigException('The "mask" property must be set.'); } - - if (!isset($this->options['id'])) { - $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); - } } /** From c1aef527e43e7a2ef1e7c613bba258e7ba235a6a Mon Sep 17 00:00:00 2001 From: Larry Ullman Date: Sat, 21 Dec 2013 20:54:54 -0500 Subject: [PATCH 50/77] Edited up to "operator can be..." --- docs/guide/query-builder.md | 91 ++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index ac79f1d..f775c76 100644 --- a/docs/guide/query-builder.md +++ b/docs/guide/query-builder.md @@ -1,86 +1,98 @@ Query Builder and Query ======================= -Yii provides a basic database access layer as was described in [Database basics](database-basics.md) section. Still it's -a bit too much to use SQL directly all the time. To solve the issue Yii provides a query builder that allows you to -work with the database in object-oriented style. +Yii provides a basic database access layer as described in the [Database basics](database-basics.md) section. The database access layer provides a low-level way to interact with the database. While useful in some situations, it can be tedious to rely too much upon direct SQL. An alternative approach that Yii provides is the Query Builder. The Query Builder provides an object-oriented vehicle for generating queries to be executed. -Basic query builder usage is the following: +Here's a basic example: ```php $query = new Query; -// Define query +// Define the query: $query->select('id, name') - ->from('tbl_user') - ->limit(10); + ->from('tbl_user') + ->limit(10); -// Create a command. You can get the actual SQL using $command->sql +// Create a command. $command = $query->createCommand(); -// Execute command +// You can get the actual SQL using $command->sql + +// Execute the command: $rows = $command->queryAll(); ``` -Basic selects and joins ------------------------ +Basic selects +------------- -In order to form a `SELECT` query you need to specify what to select and where to select it from. +In order to form a basic `SELECT` query, you need to specify what columns to select and from what table: ```php $query->select('id, name') ->from('tbl_user'); ``` -If you want to get IDs of all users with posts you can use `DISTINCT`. With query builder it will look like the following: +Select options can be specified as a comma-separated string, as in the above, or as an array. The array syntax is especially useful when forming the selection dynamically: ```php -$query->select('user_id')->distinct()->from('tbl_post'); +$columns = []; +$columns[] = 'id'; +$columns[] = 'name'; +$query->select($columns) + ->from('tbl_user'); ``` -Select options can be specified as array. It's especially useful when these are formed dynamically. - -```php -$query->select(['tbl_user.name AS author', 'tbl_post.title as title']) // <-- specified as array - ->from('tbl_user') - ->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id'); // <-- join with another table -``` +Joins +----- -In the code above we've used `leftJoin` method to select from two related tables at the same time. First parameter -specifies table name and the second is the join condition. Query builder has the following methods to join tables: +Joins are generated in the Query Builder by using the applicable join method: - `innerJoin` - `leftJoin` - `rightJoin` -If your data storage supports more types you can use generic `join` method: +This left join selects data from two related tables in one query: + +```php +$query->select(['tbl_user.name AS author', 'tbl_post.title as title']) ->from('tbl_user') + ->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id'); +``` + +In the code, the `leftJion` method's first parameter +specifies the table to join to. The second paramter defines the join condition. + +If your database application supports other join types, you can use those via the generic `join` method: ```php $query->join('FULL OUTER JOIN', 'tbl_post', 'tbl_post.user_id = tbl_user.id'); ``` -Specifying conditions +The first argument is the join type to perform. The second is the table to join to, and the third is the condition. + +Specifying SELECT conditions --------------------- -Usually you need data that matches some conditions. There are some useful methods to specify these and the most powerful -is `where`. There are multiple ways to use it. +Usually data is selected based upon certain criteria. Query Builder has some useful methods to specify these, the most powerful of which being `where`. It can be used in multiple ways. -The simplest is to specify condition in a string: +The simplest way to apply a condition is to use a string: ```php $query->where('status=:status', [':status' => $status]); ``` -When using this format make sure you're binding parameters and not creating a query by string concatenation. +When using strings, make sure you're binding the query parameters, not creating a query by string concatenation. The above approach is safe to use, the following is not: -Instead of binding status value immediately you can do it using `params` or `addParams`: +```php +$query->where("status=$status"); // Dangerous! +``` + +Instead of binding the status value immediately, you can do so using `params` or `addParams`: ```php $query->where('status=:status'); - $query->addParams([':status' => $status]); ``` -There is another convenient way to use the method called hash format: +Multiple conditions can simultaneously be set in `where` using the *hash format*: ```php $query->where([ @@ -90,19 +102,19 @@ $query->where([ ]); ``` -It will generate the following SQL: +That code will generate the following SQL: ```sql WHERE (`status` = 10) AND (`type` = 2) AND (`id` IN (4, 8, 15, 16, 23, 42)) ``` -If you'll specify value as `null` such as the following: +NULL is a special value in databases, and is handled smartly by the Query Builder. This code: ```php $query->where(['status' => null]); ``` -SQL generated will be: +results in this WHERE clause: ```sql WHERE (`status` IS NULL) @@ -174,6 +186,15 @@ $query->orderBy([ Here we are ordering by `id` ascending and then by `name` descending. +Distinct +-------- + +If you want to get IDs of all users with posts you can use `DISTINCT`. With query builder it will look like the following: + +```php +$query->select('user_id')->distinct()->from('tbl_post'); +``` + Group and Having ---------------- From 42d8748e6e9b3bab6a855dbbffab5b6afe014e88 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 21 Dec 2013 23:26:35 -0500 Subject: [PATCH 51/77] Fixes #1579: throw exception when the given AR relation name does not match in a case sensitive manner. Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` --- extensions/yii/sphinx/ActiveRecord.php | 4 +-- framework/CHANGELOG.md | 2 ++ framework/yii/db/BaseActiveRecord.php | 35 ++++++++++++++++------ .../unit/extensions/mongodb/ActiveRelationTest.php | 4 +-- .../unit/extensions/sphinx/ActiveRelationTest.php | 4 +-- .../sphinx/ExternalActiveRelationTest.php | 4 +-- tests/unit/framework/ar/ActiveRecordTestTrait.php | 6 ++-- 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/extensions/yii/sphinx/ActiveRecord.php b/extensions/yii/sphinx/ActiveRecord.php index e7bda34..0f9a48e 100644 --- a/extensions/yii/sphinx/ActiveRecord.php +++ b/extensions/yii/sphinx/ActiveRecord.php @@ -29,7 +29,7 @@ use yii\helpers\StringHelper; * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key * value is null). This property is read-only. - * @property array $populatedRelations An array of relation data indexed by relation names. This property is + * @property array $relatedRecords An array of the populated related records indexed by relation names. This property is * read-only. * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null). @@ -668,4 +668,4 @@ abstract class ActiveRecord extends BaseActiveRecord $transactions = $this->transactions(); return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); } -} \ No newline at end of file +} diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9694cb7..0d2bd31 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -21,11 +21,13 @@ Yii Framework 2 Change Log - Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe) - Enh #1523: Query conditions now allow to use the NOT operator (cebe) - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) +- Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Sort and Paginiation can now create absolute URLs (cebe) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) +- Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue) - Chg: Added `yii\widgets\InputWidget::options` (qiangxue) - New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo) diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php index dae7134..6c947b8 100644 --- a/framework/yii/db/BaseActiveRecord.php +++ b/framework/yii/db/BaseActiveRecord.php @@ -30,7 +30,7 @@ use yii\helpers\Inflector; * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key * value is null). This property is read-only. - * @property array $populatedRelations An array of relation data indexed by relation names. This property is + * @property array $relatedRecords An array of the populated related records indexed by relation names. This property is * read-only. * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null). @@ -232,6 +232,13 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } $value = parent::__get($name); if ($value instanceof ActiveRelationInterface) { + if (method_exists($this, 'get' . $name)) { + $method = new \ReflectionMethod($this, 'get' . $name); + $realName = lcfirst(substr($method->getName(), 3)); + if ($realName !== $name) { + throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\"."); + } + } return $this->_related[$name] = $value->multiple ? $value->all() : $value->one(); } else { return $value; @@ -390,10 +397,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } /** - * Returns all populated relations. - * @return array an array of relation data indexed by relation names. + * Returns all populated related records. + * @return array an array of related records indexed by relation names. */ - public function getPopulatedRelations() + public function getRelatedRecords() { return $this->_related; } @@ -999,15 +1006,25 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface { $getter = 'get' . $name; try { + // the relation could be defined in a behavior $relation = $this->$getter(); - if ($relation instanceof ActiveRelationInterface) { - return $relation; - } else { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); - } } catch (UnknownMethodException $e) { throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); } + if (!$relation instanceof ActiveRelationInterface) { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + } + + if (method_exists($this, $getter)) { + // relation name is case sensitive, trying to validate it when the relation is defined within this class + $method = new \ReflectionMethod($this, $getter); + $realName = lcfirst(substr($method->getName(), 3)); + if ($realName !== $name) { + throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\"."); + } + } + + return $relation; } /** diff --git a/tests/unit/extensions/mongodb/ActiveRelationTest.php b/tests/unit/extensions/mongodb/ActiveRelationTest.php index 8736d52..2baeab4 100644 --- a/tests/unit/extensions/mongodb/ActiveRelationTest.php +++ b/tests/unit/extensions/mongodb/ActiveRelationTest.php @@ -69,7 +69,7 @@ class ActiveRelationTest extends MongoDbTestCase $this->assertTrue($order->isRelationPopulated('customer')); $this->assertTrue($customer instanceof Customer); $this->assertEquals((string)$customer->_id, (string)$order->customer_id); - $this->assertEquals(1, count($order->populatedRelations)); + $this->assertEquals(1, count($order->relatedRecords)); } public function testFindEager() @@ -83,4 +83,4 @@ class ActiveRelationTest extends MongoDbTestCase $this->assertTrue($orders[1]->customer instanceof Customer); $this->assertEquals((string)$orders[1]->customer->_id, (string)$orders[1]->customer_id); } -} \ No newline at end of file +} diff --git a/tests/unit/extensions/sphinx/ActiveRelationTest.php b/tests/unit/extensions/sphinx/ActiveRelationTest.php index cd58035..d85c6b9 100644 --- a/tests/unit/extensions/sphinx/ActiveRelationTest.php +++ b/tests/unit/extensions/sphinx/ActiveRelationTest.php @@ -29,7 +29,7 @@ class ActiveRelationTest extends SphinxTestCase $index = $article->index; $this->assertTrue($article->isRelationPopulated('index')); $this->assertTrue($index instanceof ArticleIndex); - $this->assertEquals(1, count($article->populatedRelations)); + $this->assertEquals(1, count($article->relatedRecords)); $this->assertEquals($article->id, $index->id); } @@ -42,4 +42,4 @@ class ActiveRelationTest extends SphinxTestCase $this->assertTrue($articles[0]->index instanceof ArticleIndex); $this->assertTrue($articles[1]->index instanceof ArticleIndex); } -} \ No newline at end of file +} diff --git a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php index e30a0cf..1740c42 100644 --- a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php +++ b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php @@ -32,7 +32,7 @@ class ExternalActiveRelationTest extends SphinxTestCase $source = $article->source; $this->assertTrue($article->isRelationPopulated('source')); $this->assertTrue($source instanceof ArticleDb); - $this->assertEquals(1, count($article->populatedRelations)); + $this->assertEquals(1, count($article->relatedRecords)); // has many : /*$this->assertFalse($article->isRelationPopulated('tags')); @@ -71,4 +71,4 @@ class ExternalActiveRelationTest extends SphinxTestCase ->all(); $this->assertEquals(2, count($articles)); } -} \ No newline at end of file +} diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index 50a5f81..95def9d 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -392,14 +392,14 @@ trait ActiveRecordTestTrait $orders = $customer->orders; $this->assertTrue($customer->isRelationPopulated('orders')); $this->assertEquals(2, count($orders)); - $this->assertEquals(1, count($customer->populatedRelations)); + $this->assertEquals(1, count($customer->relatedRecords)); /** @var Customer $customer */ $customer = $this->callCustomerFind(2); $this->assertFalse($customer->isRelationPopulated('orders')); $orders = $customer->getOrders()->where(['id' => 3])->all(); $this->assertFalse($customer->isRelationPopulated('orders')); - $this->assertEquals(0, count($customer->populatedRelations)); + $this->assertEquals(0, count($customer->relatedRecords)); $this->assertEquals(1, count($orders)); $this->assertEquals(3, $orders[0]->id); @@ -421,7 +421,7 @@ trait ActiveRecordTestTrait $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one(); $this->assertTrue($customer->isRelationPopulated('orders')); $this->assertEquals(1, count($customer->orders)); - $this->assertEquals(1, count($customer->populatedRelations)); + $this->assertEquals(1, count($customer->relatedRecords)); } public function testFindLazyVia() From a08de951772603ec4333c5a3ec339af193e2f612 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 01:27:03 -0500 Subject: [PATCH 52/77] Fixes #1582: Error messages shown via client-side validation should not be double encoded --- framework/CHANGELOG.md | 1 + framework/yii/assets/yii.activeForm.js | 8 ++++---- framework/yii/captcha/CaptchaValidator.php | 4 ++-- framework/yii/validators/BooleanValidator.php | 4 ++-- framework/yii/validators/CompareValidator.php | 4 ++-- framework/yii/validators/EmailValidator.php | 4 ++-- framework/yii/validators/NumberValidator.php | 12 ++++++------ framework/yii/validators/RangeValidator.php | 4 ++-- framework/yii/validators/RegularExpressionValidator.php | 4 ++-- framework/yii/validators/RequiredValidator.php | 4 ++-- framework/yii/validators/StringValidator.php | 16 ++++++++-------- framework/yii/validators/UrlValidator.php | 4 ++-- 12 files changed, 35 insertions(+), 34 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 0d2bd31..cf6eb5a 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Bug #1509: The SQL for creating Postgres RBAC tables is incorrect (qiangxue) - Bug #1545: It was not possible to execute db Query twice, params where missing (cebe) - Bug #1550: fixed the issue that JUI input widgets did not property input IDs. +- Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue) - Bug #1591: StringValidator is accessing undefined property (qiangxue) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) diff --git a/framework/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js index c1d5bf5..e898efc 100644 --- a/framework/yii/assets/yii.activeForm.js +++ b/framework/yii/assets/yii.activeForm.js @@ -348,7 +348,7 @@ $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) .addClass(data.settings.errorCssClass); } else { - $error.html(''); + $error.text(''); $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') .addClass(data.settings.successCssClass); } @@ -365,15 +365,15 @@ var updateSummary = function ($form, messages) { var data = $form.data('yiiActiveForm'), $summary = $form.find(data.settings.errorSummary), - content = ''; + $ul = $summary.find('ul'); if ($summary.length && messages) { $.each(data.attributes, function () { if ($.isArray(messages[this.name]) && messages[this.name].length) { - content += '
  • ' + messages[this.name][0] + '
  • '; + $ul.append($('
  • ').text(messages[this.name][0])); } }); - $summary.toggle(content !== '').find('ul').html(content); + $summary.toggle($ul.find('li').length > 0); } }; diff --git a/framework/yii/captcha/CaptchaValidator.php b/framework/yii/captcha/CaptchaValidator.php index 83996d5..57665ec 100644 --- a/framework/yii/captcha/CaptchaValidator.php +++ b/framework/yii/captcha/CaptchaValidator.php @@ -93,9 +93,9 @@ class CaptchaValidator extends Validator 'hash' => $hash, 'hashKey' => 'yiiCaptcha/' . $this->captchaAction, 'caseSensitive' => $this->caseSensitive, - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), - ])), + ]), ]; if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/BooleanValidator.php b/framework/yii/validators/BooleanValidator.php index 961ed14..8bca827 100644 --- a/framework/yii/validators/BooleanValidator.php +++ b/framework/yii/validators/BooleanValidator.php @@ -72,11 +72,11 @@ class BooleanValidator extends Validator $options = [ 'trueValue' => $this->trueValue, 'falseValue' => $this->falseValue, - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), '{true}' => $this->trueValue, '{false}' => $this->falseValue, - ])), + ]), ]; if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/CompareValidator.php b/framework/yii/validators/CompareValidator.php index 69bd6d5..cbd12d2 100644 --- a/framework/yii/validators/CompareValidator.php +++ b/framework/yii/validators/CompareValidator.php @@ -195,11 +195,11 @@ class CompareValidator extends Validator $options['skipOnEmpty'] = 1; } - $options['message'] = Html::encode(strtr($this->message, [ + $options['message'] = strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), '{compareAttribute}' => $compareValue, '{compareValue}' => $compareValue, - ])); + ]); ValidationAsset::register($view); return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; diff --git a/framework/yii/validators/EmailValidator.php b/framework/yii/validators/EmailValidator.php index 24eeaec..e5d9b75 100644 --- a/framework/yii/validators/EmailValidator.php +++ b/framework/yii/validators/EmailValidator.php @@ -98,9 +98,9 @@ class EmailValidator extends Validator 'pattern' => new JsExpression($this->pattern), 'fullPattern' => new JsExpression($this->fullPattern), 'allowName' => $this->allowName, - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), - ])), + ]), 'enableIDN' => (boolean)$this->enableIDN, ]; if ($this->skipOnEmpty) { diff --git a/framework/yii/validators/NumberValidator.php b/framework/yii/validators/NumberValidator.php index 60e920a..1bb2360 100644 --- a/framework/yii/validators/NumberValidator.php +++ b/framework/yii/validators/NumberValidator.php @@ -124,24 +124,24 @@ class NumberValidator extends Validator $options = [ 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $label, - ])), + ]), ]; if ($this->min !== null) { $options['min'] = $this->min; - $options['tooSmall'] = Html::encode(strtr($this->tooSmall, [ + $options['tooSmall'] = strtr($this->tooSmall, [ '{attribute}' => $label, '{min}' => $this->min, - ])); + ]); } if ($this->max !== null) { $options['max'] = $this->max; - $options['tooBig'] = Html::encode(strtr($this->tooBig, [ + $options['tooBig'] = strtr($this->tooBig, [ '{attribute}' => $label, '{max}' => $this->max, - ])); + ]); } if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/RangeValidator.php b/framework/yii/validators/RangeValidator.php index cfd1f51..a4da139 100644 --- a/framework/yii/validators/RangeValidator.php +++ b/framework/yii/validators/RangeValidator.php @@ -73,9 +73,9 @@ class RangeValidator extends Validator $options = [ 'range' => $range, 'not' => $this->not, - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), - ])), + ]), ]; if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/RegularExpressionValidator.php b/framework/yii/validators/RegularExpressionValidator.php index 7b02381..28e9bdc 100644 --- a/framework/yii/validators/RegularExpressionValidator.php +++ b/framework/yii/validators/RegularExpressionValidator.php @@ -80,9 +80,9 @@ class RegularExpressionValidator extends Validator $options = [ 'pattern' => new JsExpression($pattern), 'not' => $this->not, - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), - ])), + ]), ]; if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/RequiredValidator.php b/framework/yii/validators/RequiredValidator.php index 43b40cf..f291f39 100644 --- a/framework/yii/validators/RequiredValidator.php +++ b/framework/yii/validators/RequiredValidator.php @@ -101,9 +101,9 @@ class RequiredValidator extends Validator $options['strict'] = 1; } - $options['message'] = Html::encode(strtr($options['message'], [ + $options['message'] = strtr($options['message'], [ '{attribute}' => $object->getAttributeLabel($attribute), - ])); + ]); ValidationAsset::register($view); return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; diff --git a/framework/yii/validators/StringValidator.php b/framework/yii/validators/StringValidator.php index dbc4001..279a189 100644 --- a/framework/yii/validators/StringValidator.php +++ b/framework/yii/validators/StringValidator.php @@ -151,31 +151,31 @@ class StringValidator extends Validator $label = $object->getAttributeLabel($attribute); $options = [ - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $label, - ])), + ]), ]; if ($this->min !== null) { $options['min'] = $this->min; - $options['tooShort'] = Html::encode(strtr($this->tooShort, [ + $options['tooShort'] = strtr($this->tooShort, [ '{attribute}' => $label, '{min}' => $this->min, - ])); + ]); } if ($this->max !== null) { $options['max'] = $this->max; - $options['tooLong'] = Html::encode(strtr($this->tooLong, [ + $options['tooLong'] = strtr($this->tooLong, [ '{attribute}' => $label, '{max}' => $this->max, - ])); + ]); } if ($this->length !== null) { $options['is'] = $this->length; - $options['notEqual'] = Html::encode(strtr($this->notEqual, [ + $options['notEqual'] = strtr($this->notEqual, [ '{attribute}' => $label, '{length}' => $this->length, - ])); + ]); } if ($this->skipOnEmpty) { $options['skipOnEmpty'] = 1; diff --git a/framework/yii/validators/UrlValidator.php b/framework/yii/validators/UrlValidator.php index 4023e2a..4cb20f6 100644 --- a/framework/yii/validators/UrlValidator.php +++ b/framework/yii/validators/UrlValidator.php @@ -121,9 +121,9 @@ class UrlValidator extends Validator $options = [ 'pattern' => new JsExpression($pattern), - 'message' => Html::encode(strtr($this->message, [ + 'message' => strtr($this->message, [ '{attribute}' => $object->getAttributeLabel($attribute), - ])), + ]), 'enableIDN' => (boolean)$this->enableIDN, ]; if ($this->skipOnEmpty) { From 46768286b17005cebd396e1156965d8d6e57a3c8 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 13:01:37 +0100 Subject: [PATCH 53/77] fixes #1593: fixed typo in Nav --- extensions/yii/bootstrap/Nav.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/bootstrap/Nav.php b/extensions/yii/bootstrap/Nav.php index ef45f09..42e6346 100644 --- a/extensions/yii/bootstrap/Nav.php +++ b/extensions/yii/bootstrap/Nav.php @@ -164,7 +164,7 @@ class Nav extends Widget if ($items !== null) { $linkOptions['data-toggle'] = 'dropdown'; Html::addCssClass($options, 'dropdown'); - Html::addCssClass($urlOptions, 'dropdown-toggle'); + Html::addCssClass($linkOptions, 'dropdown-toggle'); $label .= ' ' . Html::tag('b', '', ['class' => 'caret']); if (is_array($items)) { $items = Dropdown::widget([ From 0035a982d86e4f12d84a805e6601eabac20ba92b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 13:39:40 +0100 Subject: [PATCH 54/77] fixed typo --- docs/guide/assets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/assets.md b/docs/guide/assets.md index 47c4063..467d7f8 100644 --- a/docs/guide/assets.md +++ b/docs/guide/assets.md @@ -40,7 +40,7 @@ application's `web` directory. is an alias that corresponds to your website base URL such as `http://example.com/`. In case you have asset files under non web accessible directory, that is the case for any extension, you need -to additionally specify `$sourcePath`. Files will be copied or symlinked from source bath to base path prior to being +to additionally specify `$sourcePath`. Files will be copied or symlinked from source path to base path prior to being registered. In case source path is used `baseUrl` is generated automatically at the time of publishing asset bundle. Dependencies on other asset bundles are specified via `$depends` property. It is an array that contains fully qualified From d5f40b42cfd8467bdcfbf127fee66c6f451aea99 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 10:02:00 -0500 Subject: [PATCH 55/77] Added ActiveRecordInterface::getOldPrimaryKey(). --- framework/yii/db/ActiveRecordInterface.php | 19 ++++++++++++++++++- framework/yii/validators/ExistValidator.php | 4 ++-- framework/yii/validators/UniqueValidator.php | 8 ++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/framework/yii/db/ActiveRecordInterface.php b/framework/yii/db/ActiveRecordInterface.php index 556384b..73db852 100644 --- a/framework/yii/db/ActiveRecordInterface.php +++ b/framework/yii/db/ActiveRecordInterface.php @@ -70,6 +70,23 @@ interface ActiveRecordInterface public function getPrimaryKey($asArray = false); /** + * Returns the old primary key value(s). + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. + * If this is false (default), a scalar value will be returned for non-composite primary key. + * @property mixed The old primary key value. An array (column name => column value) is + * returned if the primary key is composite. A string is returned otherwise (null will be + * returned if the key value is null). + * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getOldPrimaryKey($asArray = false); + + /** * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. * * This method is usually ment to be used like this: @@ -290,4 +307,4 @@ interface ActiveRecordInterface * If true, the model containing the foreign key will be deleted. */ public function unlink($name, $model, $delete = false); -} \ No newline at end of file +} diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php index 585b82f..04d8af8 100644 --- a/framework/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -61,7 +61,7 @@ class ExistValidator extends Validator return; } - /** @var \yii\db\ActiveRecord $className */ + /** @var \yii\db\ActiveRecordInterface $className */ $className = $this->className === null ? get_class($object) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); @@ -85,7 +85,7 @@ class ExistValidator extends Validator if ($this->attributeName === null) { throw new InvalidConfigException('The "attributeName" property must be set.'); } - /** @var \yii\db\ActiveRecord $className */ + /** @var \yii\db\ActiveRecordInterface $className */ $className = $this->className; $query = $className::find(); $query->where([$this->attributeName => $value]); diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php index d9cd587..b04f4a9 100644 --- a/framework/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -8,8 +8,6 @@ namespace yii\validators; use Yii; -use yii\base\InvalidConfigException; -use yii\db\ActiveRecord; use yii\db\ActiveRecordInterface; /** @@ -57,7 +55,7 @@ class UniqueValidator extends Validator return; } - /** @var \yii\db\ActiveRecord $className */ + /** @var \yii\db\ActiveRecordInterface $className */ $className = $this->className === null ? get_class($object) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; @@ -69,9 +67,7 @@ class UniqueValidator extends Validator $exists = $query->exists(); } else { // if current $object is in the database already we can't use exists() - $query->limit(2); - $objects = $query->all(); - + $objects = $query->limit(2)->all(); $n = count($objects); if ($n === 1) { if (in_array($attributeName, $className::primaryKey())) { From d620f3152ef0d3663c8096c25274386c2d53f9e4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 10:19:49 -0500 Subject: [PATCH 56/77] refactored BaseActiveRecord::isPrimaryKey() --- framework/yii/db/BaseActiveRecord.php | 9 ++++----- framework/yii/validators/UniqueValidator.php | 5 +++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php index 6c947b8..e20501b 100644 --- a/framework/yii/db/BaseActiveRecord.php +++ b/framework/yii/db/BaseActiveRecord.php @@ -1234,11 +1234,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface public static function isPrimaryKey($keys) { $pks = static::primaryKey(); - foreach ($keys as $key) { - if (!in_array($key, $pks, true)) { - return false; - } + if (count($keys) === count($pks)) { + return count(array_intersect($keys, $pks)) === count($pks); + } else { + return false; } - return count($keys) === count($pks); } } diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php index b04f4a9..53b6739 100644 --- a/framework/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -55,7 +55,7 @@ class UniqueValidator extends Validator return; } - /** @var \yii\db\ActiveRecordInterface $className */ + /** @var ActiveRecordInterface $className */ $className = $this->className === null ? get_class($object) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; @@ -67,6 +67,7 @@ class UniqueValidator extends Validator $exists = $query->exists(); } else { // if current $object is in the database already we can't use exists() + /** @var ActiveRecordInterface[] $objects */ $objects = $query->limit(2)->all(); $n = count($objects); if ($n === 1) { @@ -75,7 +76,7 @@ class UniqueValidator extends Validator $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); } else { // non-primary key, need to exclude the current record based on PK - $exists = array_shift($objects)->getPrimaryKey() != $object->getOldPrimaryKey(); + $exists = $objects[0]->getPrimaryKey() != $object->getOldPrimaryKey(); } } else { $exists = $n > 1; From 2eee7b3f1bc1cbc61bac1cbae258b2f69aa5cf01 Mon Sep 17 00:00:00 2001 From: futbolim Date: Sun, 22 Dec 2013 17:36:40 +0200 Subject: [PATCH 57/77] Update ActiveField.php doc fixes --- framework/yii/widgets/ActiveField.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php index 4228ea9..bd26237 100644 --- a/framework/yii/widgets/ActiveField.php +++ b/framework/yii/widgets/ActiveField.php @@ -112,8 +112,8 @@ class ActiveField extends Component /** * @var array different parts of the field (e.g. input, label). This will be used together with * [[template]] to generate the final field HTML code. The keys are the token names in [[template]], - * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}`, - * `{error}`, and `{error}`. Note that you normally don't need to access this property directly as + * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}` and `{error}`. + * Note that you normally don't need to access this property directly as * it is maintained by various methods of this class. */ public $parts = []; From 252b6c9ef17d4401e09a60380ddd72c145dbbd72 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 11:30:59 -0500 Subject: [PATCH 58/77] Fixes #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` --- framework/CHANGELOG.md | 1 + framework/yii/validators/ExistValidator.php | 54 ++++++++++++++++++---- framework/yii/validators/UniqueValidator.php | 41 ++++++++++++++-- .../framework/validators/ExistValidatorTest.php | 42 +++++++++++++++++ .../framework/validators/UniqueValidatorTest.php | 47 +++++++++++++++++++ 5 files changed, 172 insertions(+), 13 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index cf6eb5a..fb51798 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -16,6 +16,7 @@ Yii Framework 2 Change Log - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) - Bug: Fixed issue with tabular input on ActiveField::radio() and ActiveField::checkbox() (jom) +- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue) - Enh #1293: Replaced Console::showProgress() with a better approach. See Console::startProgress() for details (cebe) - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php index 04d8af8..c205655 100644 --- a/framework/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -30,10 +30,25 @@ class ExistValidator extends Validator */ public $className; /** - * @var string the yii\db\ActiveRecord class attribute name that should be + * @var string|array the ActiveRecord class attribute name that should be * used to look for the attribute value being validated. Defaults to null, - * meaning using the name of the attribute being validated. - * @see className + * meaning using the name of the attribute being validated. Use a string + * to specify the attribute that is different from the attribute being validated + * (often used together with [[className]]). Use an array to validate the existence about + * multiple columns. For example, + * + * ```php + * // a1 needs to exist + * array('a1', 'exist') + * // a1 needs to exist, but its value will use a2 to check for the existence + * array('a1', 'exist', 'attributeName' => 'a2') + * // a1 and a2 need to exist together, and they both will receive error message + * array('a1, a2', 'exist', 'attributeName' => array('a1', 'a2')) + * // a1 and a2 need to exist together, only a1 will receive error message + * array('a1', 'exist', 'attributeName' => array('a1', 'a2')) + * // a1 and a2 need to exist together, a2 will take value 10, only a1 will receive error message + * array('a1', 'exist', 'attributeName' => array('a1', 'a2' => 10)) + * ``` */ public $attributeName; @@ -64,9 +79,7 @@ class ExistValidator extends Validator /** @var \yii\db\ActiveRecordInterface $className */ $className = $this->className === null ? get_class($object) : $this->className; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; - $query = $className::find(); - $query->where([$attributeName => $value]); - if (!$query->exists()) { + if (!$this->exists($className, $attributeName, $object, $value)) { $this->addError($object, $attribute, $this->message); } } @@ -85,10 +98,33 @@ class ExistValidator extends Validator if ($this->attributeName === null) { throw new InvalidConfigException('The "attributeName" property must be set.'); } + return $this->exists($this->className, $this->attributeName, null, $value) ? null : [$this->message, []]; + } + + /** + * Performs existence check. + * @param string $className the AR class name to be checked against + * @param string|array $attributeName the attribute(s) to be checked + * @param \yii\db\ActiveRecordInterface $object the object whose value is being validated + * @param mixed $value the attribute value currently being validated + * @return boolean whether the data being validated exists in the database already + */ + protected function exists($className, $attributeName, $object, $value) + { /** @var \yii\db\ActiveRecordInterface $className */ - $className = $this->className; $query = $className::find(); - $query->where([$this->attributeName => $value]); - return $query->exists() ? null : [$this->message, []]; + if (is_array($attributeName)) { + $params = []; + foreach ($attributeName as $k => $v) { + if (is_integer($k)) { + $params[$v] = $this->className === null && $object !== null ? $object->$v : $value; + } else { + $params[$k] = $v; + } + } + } else { + $params = [$attributeName => $value]; + } + return $query->where($params)->exists(); } } diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php index 53b6739..a497ead 100644 --- a/framework/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -26,9 +26,25 @@ class UniqueValidator extends Validator */ public $className; /** - * @var string the ActiveRecord class attribute name that should be + * @var string|array the ActiveRecord class attribute name that should be * used to look for the attribute value being validated. Defaults to null, - * meaning using the name of the attribute being validated. + * meaning using the name of the attribute being validated. Use a string + * to specify the attribute that is different from the attribute being validated + * (often used together with [[className]]). Use an array to validate uniqueness about + * multiple columns. For example, + * + * ```php + * // a1 needs to be unique + * array('a1', 'unique') + * // a1 needs to be unique, but its value will use a2 to check for the uniqueness + * array('a1', 'unique', 'attributeName' => 'a2') + * // a1 and a2 need to unique together, and they both will receive error message + * array('a1, a2', 'unique', 'attributeName' => array('a1', 'a2')) + * // a1 and a2 need to unique together, only a1 will receive error message + * array('a1', 'unique', 'attributeName' => array('a1', 'a2')) + * // a1 and a2 need to unique together, a2 will take value 10, only a1 will receive error message + * array('a1', 'unique', 'attributeName' => array('a1', 'a2' => 10)) + * ``` */ public $attributeName; @@ -60,7 +76,20 @@ class UniqueValidator extends Validator $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); - $query->where([$attributeName => $value]); + + if (is_array($attributeName)) { + $params = []; + foreach ($attributeName as $k => $v) { + if (is_integer($k)) { + $params[$v] = $this->className === null ? $object->$v : $value; + } else { + $params[$k] = $v; + } + } + } else { + $params = [$attributeName => $value]; + } + $query->where($params); if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { // if current $object isn't in the database yet then it's OK just to call exists() @@ -71,7 +100,11 @@ class UniqueValidator extends Validator $objects = $query->limit(2)->all(); $n = count($objects); if ($n === 1) { - if (in_array($attributeName, $className::primaryKey())) { + $keys = array_keys($params); + $pks = $className::primaryKey(); + sort($keys); + sort($pks); + if ($keys === $pks) { // primary key is modified and not unique $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); } else { diff --git a/tests/unit/framework/validators/ExistValidatorTest.php b/tests/unit/framework/validators/ExistValidatorTest.php index 45ff5d5..03332ad 100644 --- a/tests/unit/framework/validators/ExistValidatorTest.php +++ b/tests/unit/framework/validators/ExistValidatorTest.php @@ -7,6 +7,8 @@ use Yii; use yii\base\Exception; use yii\validators\ExistValidator; use yiiunit\data\ar\ActiveRecord; +use yiiunit\data\ar\Order; +use yiiunit\data\ar\OrderItem; use yiiunit\data\validators\models\ValidatorTestMainModel; use yiiunit\data\validators\models\ValidatorTestRefModel; use yiiunit\framework\db\DatabaseTestCase; @@ -92,4 +94,44 @@ class ExistValidatorTest extends DatabaseTestCase $val->validateAttribute($m, 'test_val'); $this->assertTrue($m->hasErrors('test_val')); } + + public function testValidateCompositeKeys() + { + $val = new ExistValidator([ + 'className' => OrderItem::className(), + 'attributeName' => ['order_id', 'item_id'], + ]); + // validate old record + $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + + // validate new record + $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); + + $val = new ExistValidator([ + 'className' => OrderItem::className(), + 'attributeName' => ['order_id', 'item_id' => 2], + ]); + // validate old record + $m = Order::find(1); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = Order::find(1); + $m->id = 10; + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + + $m = new Order(['id' => 1]); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m = new Order(['id' => 10]); + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + } } diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php index 707239c..1631243 100644 --- a/tests/unit/framework/validators/UniqueValidatorTest.php +++ b/tests/unit/framework/validators/UniqueValidatorTest.php @@ -6,6 +6,8 @@ namespace yiiunit\framework\validators; use yii\validators\UniqueValidator; use Yii; use yiiunit\data\ar\ActiveRecord; +use yiiunit\data\ar\Order; +use yiiunit\data\ar\OrderItem; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\data\validators\models\ValidatorTestMainModel; use yiiunit\data\validators\models\ValidatorTestRefModel; @@ -85,4 +87,49 @@ class UniqueValidatorTest extends DatabaseTestCase $m = new ValidatorTestMainModel(); $val->validateAttribute($m, 'testMainVal'); } + + public function testValidateCompositeKeys() + { + $val = new UniqueValidator([ + 'className' => OrderItem::className(), + 'attributeName' => ['order_id', 'item_id'], + ]); + // validate old record + $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + $m->item_id = 1; + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); + + // validate new record + $m = new OrderItem(['order_id' => 1, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertTrue($m->hasErrors('order_id')); + $m = new OrderItem(['order_id' => 10, 'item_id' => 2]); + $val->validateAttribute($m, 'order_id'); + $this->assertFalse($m->hasErrors('order_id')); + + $val = new UniqueValidator([ + 'className' => OrderItem::className(), + 'attributeName' => ['order_id', 'item_id' => 2], + ]); + // validate old record + $m = Order::find(1); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m->id = 2; + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + $m->id = 3; + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + + $m = new Order(['id' => 1]); + $val->validateAttribute($m, 'id'); + $this->assertTrue($m->hasErrors('id')); + $m = new Order(['id' => 10]); + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); + } } From a7cf6a984c609117b2ba2090ce9c62feccdb1326 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 20:15:57 +0100 Subject: [PATCH 59/77] Fixes #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly --- apps/advanced/backend/config/main.php | 1 + apps/advanced/frontend/config/main.php | 1 + apps/basic/config/web.php | 1 + framework/CHANGELOG.md | 1 + 4 files changed, 4 insertions(+) diff --git a/apps/advanced/backend/config/main.php b/apps/advanced/backend/config/main.php index c745a1f..d1a45a7 100644 --- a/apps/advanced/backend/config/main.php +++ b/apps/advanced/backend/config/main.php @@ -22,6 +22,7 @@ return [ 'mail' => $params['components.mail'], 'user' => [ 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php index 6ee8ae5..2a0f330 100644 --- a/apps/advanced/frontend/config/main.php +++ b/apps/advanced/frontend/config/main.php @@ -23,6 +23,7 @@ return [ 'mail' => $params['components.mail'], 'user' => [ 'identityClass' => 'common\models\User', + 'enableAutoLogin' => true, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, diff --git a/apps/basic/config/web.php b/apps/basic/config/web.php index 472f842..e142855 100644 --- a/apps/basic/config/web.php +++ b/apps/basic/config/web.php @@ -12,6 +12,7 @@ $config = [ ], 'user' => [ 'identityClass' => 'app\models\User', + 'enableAutoLogin' => true, ], 'errorHandler' => [ 'errorAction' => 'site/error', diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index fb51798..7ef506d 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -12,6 +12,7 @@ Yii Framework 2 Change Log - Bug #1550: fixed the issue that JUI input widgets did not property input IDs. - Bug #1582: Error messages shown via client-side validation should not be double encoded (qiangxue) - Bug #1591: StringValidator is accessing undefined property (qiangxue) +- Bug #1597: Added `enableAutoLogin` to basic and advanced application templates so "remember me" now works properly (samdark) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) From be5afe7da886785eeda4d0417c5324a6635bc6d8 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 22:06:11 +0100 Subject: [PATCH 60/77] Fixes #1572: Added `yii\web\Controller::createAbsoluteUrl()` --- framework/CHANGELOG.md | 1 + framework/yii/web/Controller.php | 59 +++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 7ef506d..6c81dd9 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -24,6 +24,7 @@ Yii Framework 2 Change Log - Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe) - Enh #1523: Query conditions now allow to use the NOT operator (cebe) - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) +- Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 0df48bd..540140f 100644 --- a/framework/yii/web/Controller.php +++ b/framework/yii/web/Controller.php @@ -101,9 +101,9 @@ class Controller extends \yii\base\Controller } /** - * Creates a URL using the given route and parameters. + * Normalizes route making it suitable for UrlManager. Absolute routes are staying as is + * while relative routes are converted to absolute routes. * - * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. * A relative route is a route without a leading slash, such as "view", "post/view". * * - If the route is an empty string, the current [[route]] will be used; @@ -112,13 +112,10 @@ class Controller extends \yii\base\Controller * - If the route has no leading slash, it is considered to be a route relative * to the current module and will be prepended with the module's uniqueId. * - * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL. - * * @param string $route the route. This can be either an absolute route or a relative route. - * @param array $params the parameters (name-value pairs) to be included in the generated URL - * @return string the created URL + * @return string normalized route suitable for UrlManager */ - public function createUrl($route, $params = []) + protected function getNormalizedRoute($route) { if (strpos($route, '/') === false) { // empty or an action ID @@ -127,10 +124,58 @@ class Controller extends \yii\base\Controller // relative to module $route = ltrim($this->module->getUniqueId() . '/' . $route, '/'); } + return $route; + } + + /** + * Creates a relative URL using the given route and parameters. + * + * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. + * A relative route is a route without a leading slash, such as "view", "post/view". + * + * - If the route is an empty string, the current [[route]] will be used; + * - If the route contains no slashes at all, it is considered to be an action ID + * of the current controller and will be prepended with [[uniqueId]]; + * - If the route has no leading slash, it is considered to be a route relative + * to the current module and will be prepended with the module's uniqueId. + * + * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created relative URL + */ + public function createUrl($route, $params = []) + { + $route = $this->getNormalizedRoute($route); return Yii::$app->getUrlManager()->createUrl($route, $params); } /** + * Creates an absolute URL using the given route and parameters. + * + * This method enhances [[UrlManager::createAbsoluteUrl()]] by supporting relative routes. + * A relative route is a route without a leading slash, such as "view", "post/view". + * + * - If the route is an empty string, the current [[route]] will be used; + * - If the route contains no slashes at all, it is considered to be an action ID + * of the current controller and will be prepended with [[uniqueId]]; + * - If the route has no leading slash, it is considered to be a route relative + * to the current module and will be prepended with the module's uniqueId. + * + * After this route conversion, the method calls [[UrlManager::createUrl()]] to create a URL. + * + * @param string $route the route. This can be either an absolute route or a relative route. + * @param array $params the parameters (name-value pairs) to be included in the generated URL + * @return string the created absolute URL + */ + public function createAbsoluteUrl($route, $params = []) + { + $route = $this->getNormalizedRoute($route); + return Yii::$app->getUrlManager()->createAbsoluteUrl($route, $params); + } + + /** * Returns the canonical URL of the currently requested page. * The canonical URL is constructed using [[route]] and [[actionParams]]. You may use the following code * in the layout view to add a link tag about canonical URL: From 9649a6727ade3f065f3fa0b5f1fbd65e269b51c1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 16:40:51 -0500 Subject: [PATCH 61/77] Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator`. Refactored UniqueValidator and ExistValidator. --- docs/guide/validation.md | 12 ++- framework/CHANGELOG.md | 1 + framework/yii/db/ActiveQuery.php | 5 + framework/yii/validators/ExistValidator.php | 118 ++++++++++----------- framework/yii/validators/UniqueValidator.php | 94 ++++++++-------- .../framework/validators/ExistValidatorTest.php | 24 ++--- .../framework/validators/UniqueValidatorTest.php | 22 ++-- 7 files changed, 139 insertions(+), 137 deletions(-) diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 1b1e92e..5067cb6 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -67,9 +67,9 @@ Validates that the attribute value is a valid email address. Validates that the attribute value exists in a table. -- `className` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being +- `targetClass` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being validated. _(ActiveRecord class of the attribute being validated)_ -- `attributeName` the ActiveRecord attribute name that should be used to look for the attribute value being validated. +- `targetAttribute` the ActiveRecord attribute name that should be used to look for the attribute value being validated. _(name of the attribute being validated)_ ### `file`: [[FileValidator]] @@ -112,7 +112,9 @@ Validates that the attribute value is among a list of values. ### `inline`: [[InlineValidator]] -Uses a custom function to validate the attribute. You need to define a public method in your model class which will evaluate the validity of the attribute. For example, if an attribute needs to be divisible by 10. In the rules you would define: `['attributeName', 'myValidationMethod'],`. +Uses a custom function to validate the attribute. You need to define a public method in your +model class which will evaluate the validity of the attribute. For example, if an attribute +needs to be divisible by 10. In the rules you would define: `['attributeName', 'myValidationMethod'],`. Then, your own method could look like this: ```php @@ -161,9 +163,9 @@ Validates that the attribute value is of certain length. Validates that the attribute value is unique in the corresponding database table. -- `className` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being +- `targetClass` the ActiveRecord class name or alias of the class that should be used to look for the attribute value being validated. _(ActiveRecord class of the attribute being validated)_ -- `attributeName` the ActiveRecord attribute name that should be used to look for the attribute value being validated. +- `targetAttribute` the ActiveRecord attribute name that should be used to look for the attribute value being validated. _(name of the attribute being validated)_ ### `url`: [[UrlValidator]] diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 6c81dd9..41d8f75 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -32,6 +32,7 @@ Yii Framework 2 Change Log - Enh: Sort and Paginiation can now create absolute URLs (cebe) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) - Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue) +- Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue) - Chg: Added `yii\widgets\InputWidget::options` (qiangxue) - New #1438: [MongoDB integration](https://github.com/yiisoft/yii2-mongodb) ActiveRecord and Query (klimov-paul) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index e93e9be..e0e14cf 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -143,4 +143,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface } return $db->createCommand($sql, $params); } + + public function joinWith($name) + { + + } } diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php index c205655..323172a 100644 --- a/framework/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -13,44 +13,47 @@ use yii\base\InvalidConfigException; /** * ExistValidator validates that the attribute value exists in a table. * + * ExistValidator checks if the value being validated can be found in the table column specified by + * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]]. + * * This validator is often used to verify that a foreign key contains a value * that can be found in the foreign table. * + * The followings are examples of validation rules using this validator: + * + * ```php + * // a1 needs to exist + * ['a1', 'exist'] + * // a1 needs to exist, but its value will use a2 to check for the existence + * ['a1', 'exist', 'targetAttribute' => 'a2'] + * // a1 and a2 need to exist together, and they both will receive error message + * ['a1, a2', 'exist', 'targetAttribute' => ['a1', 'a2']] + * // a1 and a2 need to exist together, only a1 will receive error message + * ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']] + * // a1 needs to be unique by checking the existence of both a2 and a3 (using a1 value) + * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']] + * ``` + * * @author Qiang Xue * @since 2.0 */ class ExistValidator extends Validator { /** - * @var string the ActiveRecord class name or alias of the class - * that should be used to look for the attribute value being validated. - * Defaults to null, meaning using the ActiveRecord class of - * the attribute being validated. - * @see attributeName + * @var string the name of the ActiveRecord class that should be used to validate the existence + * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. + * @see targetAttribute */ - public $className; + public $targetClass; /** - * @var string|array the ActiveRecord class attribute name that should be - * used to look for the attribute value being validated. Defaults to null, - * meaning using the name of the attribute being validated. Use a string - * to specify the attribute that is different from the attribute being validated - * (often used together with [[className]]). Use an array to validate the existence about - * multiple columns. For example, - * - * ```php - * // a1 needs to exist - * array('a1', 'exist') - * // a1 needs to exist, but its value will use a2 to check for the existence - * array('a1', 'exist', 'attributeName' => 'a2') - * // a1 and a2 need to exist together, and they both will receive error message - * array('a1, a2', 'exist', 'attributeName' => array('a1', 'a2')) - * // a1 and a2 need to exist together, only a1 will receive error message - * array('a1', 'exist', 'attributeName' => array('a1', 'a2')) - * // a1 and a2 need to exist together, a2 will take value 10, only a1 will receive error message - * array('a1', 'exist', 'attributeName' => array('a1', 'a2' => 10)) - * ``` + * @var string|array the name of the ActiveRecord attribute that should be used to + * validate the existence of the current attribute value. If not set, it will use the name + * of the attribute currently being validated. You may use an array to validate the existence + * of multiple columns at the same time. The array values are the attributes that will be + * used to validate the existence, while the array keys are the attributes whose values are to be validated. + * If the key and the value are the same, you can just specify the value. */ - public $attributeName; + public $targetAttribute; /** @@ -69,17 +72,28 @@ class ExistValidator extends Validator */ public function validateAttribute($object, $attribute) { - $value = $object->$attribute; + /** @var \yii\db\ActiveRecordInterface $targetClass */ + $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; + $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; - if (is_array($value)) { - $this->addError($object, $attribute, $this->message); - return; + if (is_array($targetAttribute)) { + $params = []; + foreach ($targetAttribute as $k => $v) { + $params[$v] = is_integer($k) ? $object->$v : $object->$k; + } + } else { + $params = [$targetAttribute => $object->$attribute]; + } + + foreach ($params as $value) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); + return; + } } /** @var \yii\db\ActiveRecordInterface $className */ - $className = $this->className === null ? get_class($object) : $this->className; - $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; - if (!$this->exists($className, $attributeName, $object, $value)) { + if (!$targetClass::find()->where($params)->exists()) { $this->addError($object, $attribute, $this->message); } } @@ -92,39 +106,17 @@ class ExistValidator extends Validator if (is_array($value)) { return [$this->message, []]; } - if ($this->className === null) { + if ($this->targetClass === null) { throw new InvalidConfigException('The "className" property must be set.'); } - if ($this->attributeName === null) { - throw new InvalidConfigException('The "attributeName" property must be set.'); + if (!is_string($this->targetAttribute)) { + throw new InvalidConfigException('The "attributeName" property must be configured as a string.'); } - return $this->exists($this->className, $this->attributeName, null, $value) ? null : [$this->message, []]; - } - /** - * Performs existence check. - * @param string $className the AR class name to be checked against - * @param string|array $attributeName the attribute(s) to be checked - * @param \yii\db\ActiveRecordInterface $object the object whose value is being validated - * @param mixed $value the attribute value currently being validated - * @return boolean whether the data being validated exists in the database already - */ - protected function exists($className, $attributeName, $object, $value) - { - /** @var \yii\db\ActiveRecordInterface $className */ - $query = $className::find(); - if (is_array($attributeName)) { - $params = []; - foreach ($attributeName as $k => $v) { - if (is_integer($k)) { - $params[$v] = $this->className === null && $object !== null ? $object->$v : $value; - } else { - $params[$k] = $v; - } - } - } else { - $params = [$attributeName => $value]; - } - return $query->where($params)->exists(); + /** @var \yii\db\ActiveRecordInterface $targetClass */ + $targetClass = $this->targetClass; + $query = $targetClass::find(); + $query->where([$this->targetAttribute => $value]); + return $query->exists() ? null : [$this->message, []]; } } diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php index a497ead..1136f02 100644 --- a/framework/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -11,7 +11,25 @@ use Yii; use yii\db\ActiveRecordInterface; /** - * UniqueValidator validates that the attribute value is unique in the corresponding database table. + * UniqueValidator validates that the attribute value is unique in the specified database table. + * + * UniqueValidator checks if the value being validated is unique in the table column specified by + * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]]. + * + * The followings are examples of validation rules using this validator: + * + * ```php + * // a1 needs to be unique + * ['a1', 'unique'] + * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value + * ['a1', 'unique', 'targetAttribute' => 'a2'] + * // a1 and a2 need to unique together, and they both will receive error message + * ['a1, a2', 'unique', 'targetAttribute' => ['a1', 'a2']] + * // a1 and a2 need to unique together, only a1 will receive error message + * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']] + * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value) + * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']] + * ``` * * @author Qiang Xue * @since 2.0 @@ -19,34 +37,20 @@ use yii\db\ActiveRecordInterface; class UniqueValidator extends Validator { /** - * @var string the ActiveRecord class name or alias of the class - * that should be used to look for the attribute value being validated. - * Defaults to null, meaning using the ActiveRecord class of the attribute being validated. - * @see attributeName + * @var string the name of the ActiveRecord class that should be used to validate the uniqueness + * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated. + * @see targetAttribute */ - public $className; + public $targetClass; /** - * @var string|array the ActiveRecord class attribute name that should be - * used to look for the attribute value being validated. Defaults to null, - * meaning using the name of the attribute being validated. Use a string - * to specify the attribute that is different from the attribute being validated - * (often used together with [[className]]). Use an array to validate uniqueness about - * multiple columns. For example, - * - * ```php - * // a1 needs to be unique - * array('a1', 'unique') - * // a1 needs to be unique, but its value will use a2 to check for the uniqueness - * array('a1', 'unique', 'attributeName' => 'a2') - * // a1 and a2 need to unique together, and they both will receive error message - * array('a1, a2', 'unique', 'attributeName' => array('a1', 'a2')) - * // a1 and a2 need to unique together, only a1 will receive error message - * array('a1', 'unique', 'attributeName' => array('a1', 'a2')) - * // a1 and a2 need to unique together, a2 will take value 10, only a1 will receive error message - * array('a1', 'unique', 'attributeName' => array('a1', 'a2' => 10)) - * ``` + * @var string|array the name of the ActiveRecord attribute that should be used to + * validate the uniqueness of the current attribute value. If not set, it will use the name + * of the attribute currently being validated. You may use an array to validate the uniqueness + * of multiple columns at the same time. The array values are the attributes that will be + * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated. + * If the key and the value are the same, you can just specify the value. */ - public $attributeName; + public $targetAttribute; /** * @inheritdoc @@ -64,31 +68,27 @@ class UniqueValidator extends Validator */ public function validateAttribute($object, $attribute) { - $value = $object->$attribute; - - if (is_array($value)) { - $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); - return; - } - - /** @var ActiveRecordInterface $className */ - $className = $this->className === null ? get_class($object) : $this->className; - $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; - - $query = $className::find(); + /** @var ActiveRecordInterface $targetClass */ + $targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass; + $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute; - if (is_array($attributeName)) { + if (is_array($targetAttribute)) { $params = []; - foreach ($attributeName as $k => $v) { - if (is_integer($k)) { - $params[$v] = $this->className === null ? $object->$v : $value; - } else { - $params[$k] = $v; - } + foreach ($targetAttribute as $k => $v) { + $params[$v] = is_integer($k) ? $object->$v : $object->$k; } } else { - $params = [$attributeName => $value]; + $params = [$targetAttribute => $object->$attribute]; + } + + foreach ($params as $value) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); + return; + } } + + $query = $targetClass::find(); $query->where($params); if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { @@ -101,7 +101,7 @@ class UniqueValidator extends Validator $n = count($objects); if ($n === 1) { $keys = array_keys($params); - $pks = $className::primaryKey(); + $pks = $targetClass::primaryKey(); sort($keys); sort($pks); if ($keys === $pks) { diff --git a/tests/unit/framework/validators/ExistValidatorTest.php b/tests/unit/framework/validators/ExistValidatorTest.php index 03332ad..8f1a054 100644 --- a/tests/unit/framework/validators/ExistValidatorTest.php +++ b/tests/unit/framework/validators/ExistValidatorTest.php @@ -36,18 +36,18 @@ class ExistValidatorTest extends DatabaseTestCase } // combine to save the time creating a new db-fixture set (likely ~5 sec) try { - $val = new ExistValidator(['className' => ValidatorTestMainModel::className()]); + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className()]); $val->validate('ref'); $this->fail('Exception should have been thrown at this time'); } catch (Exception $e) { $this->assertInstanceOf('yii\base\InvalidConfigException', $e); - $this->assertEquals('The "attributeName" property must be set.', $e->getMessage()); + $this->assertEquals('The "attributeName" property must be configured as a string.', $e->getMessage()); } } public function testValidateValue() { - $val = new ExistValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'id']); + $val = new ExistValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'id']); $this->assertTrue($val->validate(2)); $this->assertTrue($val->validate(5)); $this->assertFalse($val->validate(99)); @@ -57,22 +57,22 @@ class ExistValidatorTest extends DatabaseTestCase public function testValidateAttribute() { // existing value on different table - $val = new ExistValidator(['className' => ValidatorTestMainModel::className(), 'attributeName' => 'id']); + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); $m = ValidatorTestRefModel::find(['id' => 1]); $val->validateAttribute($m, 'ref'); $this->assertFalse($m->hasErrors()); // non-existing value on different table - $val = new ExistValidator(['className' => ValidatorTestMainModel::className(), 'attributeName' => 'id']); + $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className(), 'targetAttribute' => 'id']); $m = ValidatorTestRefModel::find(['id' => 6]); $val->validateAttribute($m, 'ref'); $this->assertTrue($m->hasErrors('ref')); // existing value on same table - $val = new ExistValidator(['attributeName' => 'ref']); + $val = new ExistValidator(['targetAttribute' => 'ref']); $m = ValidatorTestRefModel::find(['id' => 2]); $val->validateAttribute($m, 'test_val'); $this->assertFalse($m->hasErrors()); // non-existing value on same table - $val = new ExistValidator(['attributeName' => 'ref']); + $val = new ExistValidator(['targetAttribute' => 'ref']); $m = ValidatorTestRefModel::find(['id' => 5]); $val->validateAttribute($m, 'test_val_fail'); $this->assertTrue($m->hasErrors('test_val_fail')); @@ -88,7 +88,7 @@ class ExistValidatorTest extends DatabaseTestCase $val->validateAttribute($m, 'a_field'); $this->assertTrue($m->hasErrors('a_field')); // check array - $val = new ExistValidator(['attributeName' => 'ref']); + $val = new ExistValidator(['targetAttribute' => 'ref']); $m = ValidatorTestRefModel::find(['id' => 2]); $m->test_val = [1,2,3]; $val->validateAttribute($m, 'test_val'); @@ -98,8 +98,8 @@ class ExistValidatorTest extends DatabaseTestCase public function testValidateCompositeKeys() { $val = new ExistValidator([ - 'className' => OrderItem::className(), - 'attributeName' => ['order_id', 'item_id'], + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['order_id', 'item_id'], ]); // validate old record $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); @@ -115,8 +115,8 @@ class ExistValidatorTest extends DatabaseTestCase $this->assertTrue($m->hasErrors('order_id')); $val = new ExistValidator([ - 'className' => OrderItem::className(), - 'attributeName' => ['order_id', 'item_id' => 2], + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['id' => 'order_id'], ]); // validate old record $m = Order::find(1); diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php index 1631243..4af3d29 100644 --- a/tests/unit/framework/validators/UniqueValidatorTest.php +++ b/tests/unit/framework/validators/UniqueValidatorTest.php @@ -60,7 +60,7 @@ class UniqueValidatorTest extends DatabaseTestCase public function testValidateAttributeOfNonARModel() { - $val = new UniqueValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref']); + $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); $m = FakedValidationModel::createWithAttributes(['attr_1' => 5, 'attr_2' => 1313]); $val->validateAttribute($m, 'attr_1'); $this->assertTrue($m->hasErrors('attr_1')); @@ -70,7 +70,7 @@ class UniqueValidatorTest extends DatabaseTestCase public function testValidateNonDatabaseAttribute() { - $val = new UniqueValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref']); + $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']); $m = ValidatorTestMainModel::find(1); $val->validateAttribute($m, 'testMainVal'); $this->assertFalse($m->hasErrors('testMainVal')); @@ -91,8 +91,8 @@ class UniqueValidatorTest extends DatabaseTestCase public function testValidateCompositeKeys() { $val = new UniqueValidator([ - 'className' => OrderItem::className(), - 'attributeName' => ['order_id', 'item_id'], + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['order_id', 'item_id'], ]); // validate old record $m = OrderItem::find(['order_id' => 1, 'item_id' => 2]); @@ -111,19 +111,21 @@ class UniqueValidatorTest extends DatabaseTestCase $this->assertFalse($m->hasErrors('order_id')); $val = new UniqueValidator([ - 'className' => OrderItem::className(), - 'attributeName' => ['order_id', 'item_id' => 2], + 'targetClass' => OrderItem::className(), + 'targetAttribute' => ['id' => 'order_id'], ]); // validate old record $m = Order::find(1); $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); + $this->assertTrue($m->hasErrors('id')); + $m = Order::find(1); $m->id = 2; $val->validateAttribute($m, 'id'); - $this->assertFalse($m->hasErrors('id')); - $m->id = 3; - $val->validateAttribute($m, 'id'); $this->assertTrue($m->hasErrors('id')); + $m = Order::find(1); + $m->id = 10; + $val->validateAttribute($m, 'id'); + $this->assertFalse($m->hasErrors('id')); $m = new Order(['id' => 1]); $val->validateAttribute($m, 'id'); From ed337347e008c332272cd863e481fba6114d7e30 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 23:32:34 +0100 Subject: [PATCH 62/77] Updated doc on version numbering to match current tag --- docs/internals/versions.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/internals/versions.md b/docs/internals/versions.md index ba349f6..fe21fd7 100644 --- a/docs/internals/versions.md +++ b/docs/internals/versions.md @@ -1,12 +1,35 @@ Yii version numbering ===================== +Releases +-------- + A.B.C A = For Yii2 it's always 2. B = Major version. Non-BC changes with upgrade instructions. C = BC changes and additions. -A.B.CrcD +Release candidates +------------------ + +A.B.C-rc +A.B.C-rc2 + +This is when we want to do a release candidate. RC number increments till we're getting a stable release with no +critical bugs and backwards incompatibility reports. + +Alphas and betas +---------------- + +A.B.C-alpha +A.B.C-alpha2 + +Alphas are unstable versions where significant bugs may and probably do exist. API isn't fixed yet and may be changed +significantly. `alpha2` etc. may or may not be released based on overall stability of code and API. + +A.B.C-beta +A.B.C-beta2 -This is when we want to release release candidate. D is the RC number. Starts with 1 and increments till we're getting a stable release with no critical bugs and BC incompatibility reports. \ No newline at end of file +Beta is more or less stable with less bugs and API instability than alphas. There still could be changes in API but +there should be a significant reason for it. From 1f2972aa1e203ae70e9c7313e84e13ba0b6b8dcd Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 23:36:01 +0100 Subject: [PATCH 63/77] Fixed mistyped TDB -> TBD --- docs/guide/apps-own.md | 2 +- docs/guide/controller.md | 2 +- docs/guide/testing.md | 2 +- docs/guide/theming.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/apps-own.md b/docs/guide/apps-own.md index ebf7597..3ebf83f 100644 --- a/docs/guide/apps-own.md +++ b/docs/guide/apps-own.md @@ -1,4 +1,4 @@ Creating your own Application structure ======================================= -TDB \ No newline at end of file +TBD \ No newline at end of file diff --git a/docs/guide/controller.md b/docs/guide/controller.md index c07968b..801df69 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -206,7 +206,7 @@ Two other filters, [[PageCache]] and [[HttpCache]] are described in [caching](ca Catching all incoming requests ------------------------------ -TDB +TBD See also -------- diff --git a/docs/guide/testing.md b/docs/guide/testing.md index 4b88a9a..1395d17 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -1,4 +1,4 @@ Testing ======= -TDB \ No newline at end of file +TBD \ No newline at end of file diff --git a/docs/guide/theming.md b/docs/guide/theming.md index 308316a..7be1292 100644 --- a/docs/guide/theming.md +++ b/docs/guide/theming.md @@ -1,4 +1,4 @@ Theming ======= -TDB \ No newline at end of file +TBD \ No newline at end of file From bbf4eb325e259aee02a3fe90e6decd6dad1530eb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 22 Dec 2013 23:49:02 +0100 Subject: [PATCH 64/77] Added cache dependency docs to the guide --- docs/guide/caching.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/guide/caching.md b/docs/guide/caching.md index e36ae00..23bb872 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -152,7 +152,33 @@ $value2 = $cache['var2']; // equivalent to: $value2 = $cache->get('var2'); ### Cache Dependency -TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.data#cache-dependency +Besides expiration setting, cached data may also be invalidated according to some dependency changes. For example, if we +are caching the content of some file and the file is changed, we should invalidate the cached copy and read the latest +content from the file instead of the cache. + +We represent a dependency as an instance of [[\yii\caching\Dependency]] or its child class. We pass the dependency +instance along with the data to be cached when calling `set()`. + +```php +use yii\cache\FileDependency; + +// the value will expire in 30 seconds +// it may also be invalidated earlier if the dependent file is changed +Yii::$app->cache->set($id, $value, 30, new FileDependency(['fileName' => 'example.txt'])); +``` + +Now if we retrieve $value from cache by calling `get()`, the dependency will be evaluated and if it is changed, we will +get a false value, indicating the data needs to be regenerated. + +Below is a summary of the available cache dependencies: + +- [[\yii\cache\FileDependency]]: the dependency is changed if the file's last modification time is changed. +- [[\yii\cache\GroupDependency]]: marks a cached data item with a group name. You may invalidate the cached data items + with the same group name all at once by calling [[\yii\cache\GroupDependency::invalidate()]]. +- [[\yii\cache\DbDependency]]: the dependency is changed if the query result of the specified SQL statement is changed. +- [[\yii\cache\ChainedDependency]]: the dependency is changed if any of the dependencies on the chain is changed. +- [[\yii\cache\ExpressionDependency]]: the dependency is changed if the result of the specified PHP expression is + changed. ### Query Caching From 69cb09dbf34642e4069f8e69054d3667f84586ee Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 22 Dec 2013 22:49:44 -0500 Subject: [PATCH 65/77] doc fix. --- framework/yii/validators/ExistValidator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/yii/validators/ExistValidator.php b/framework/yii/validators/ExistValidator.php index 323172a..7e783a8 100644 --- a/framework/yii/validators/ExistValidator.php +++ b/framework/yii/validators/ExistValidator.php @@ -30,8 +30,8 @@ use yii\base\InvalidConfigException; * ['a1, a2', 'exist', 'targetAttribute' => ['a1', 'a2']] * // a1 and a2 need to exist together, only a1 will receive error message * ['a1', 'exist', 'targetAttribute' => ['a1', 'a2']] - * // a1 needs to be unique by checking the existence of both a2 and a3 (using a1 value) - * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']] + * // a1 needs to exist by checking the existence of both a2 and a3 (using a1 value) + * ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']] * ``` * * @author Qiang Xue From 488918d03c1e584be6003969700263358a790499 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 23 Dec 2013 12:00:12 +0100 Subject: [PATCH 66/77] Refinement in comments I checked this using XDebug, and the function actually returns null (which is something different than false). I assume it is the comment that should be changed, and not the code itself... --- framework/yii/db/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index ee24c2f..2baa78c 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -148,7 +148,7 @@ class Query extends Component implements QueryInterface * Executes the query and returns a single row of result. * @param Connection $db the database connection used to generate the SQL statement. * If this parameter is not given, the `db` application component will be used. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * @return array|boolean the first row (in terms of an array) of the query result. Null is returned if the query * results in nothing. */ public function one($db = null) From 55deceb0619be9eb739aa761fe219e1b990f2a50 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 23 Dec 2013 08:47:30 -0500 Subject: [PATCH 67/77] Fixes #1076 --- framework/yii/db/mssql/QueryBuilder.php | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php index 338a74b..77b9532 100644 --- a/framework/yii/db/mssql/QueryBuilder.php +++ b/framework/yii/db/mssql/QueryBuilder.php @@ -60,6 +60,47 @@ class QueryBuilder extends \yii\db\QueryBuilder // } /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($table, $newName) + { + return "sp_rename '$table', '$newName'"; + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + */ + public function renameColumn($table, $name, $newName) + { + return "sp_rename '$table.$name', '$newName', 'COLUMN'"; + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + $type=$this->getColumnType($type); + $sql='ALTER TABLE ' . $this->db->quoteTableName($table) . ' ALTER COLUMN ' + . $this->db->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + return $sql; + } + + /** * Builds a SQL statement for enabling or disabling integrity check. * @param boolean $check whether to turn on or off the integrity check. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. From c04e650799b6f6ff8b3a0a1d94dcce73d2cfd53e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 23 Dec 2013 09:04:26 -0500 Subject: [PATCH 68/77] Fixed composer about yii2-dev installation --- extensions/yii/composer/Installer.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/yii/composer/Installer.php b/extensions/yii/composer/Installer.php index 6f69afc..04d9d08 100644 --- a/extensions/yii/composer/Installer.php +++ b/extensions/yii/composer/Installer.php @@ -44,7 +44,7 @@ class Installer extends LibraryInstaller $this->addPackage($package); // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does if ($package->getName() == 'yiisoft/yii2-dev') { - $this->linkYiiBaseFiles(); + $this->linkBaseYiiFiles(); } } @@ -58,7 +58,7 @@ class Installer extends LibraryInstaller $this->addPackage($target); // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does if ($initial->getName() == 'yiisoft/yii2-dev') { - $this->linkYiiBaseFiles(); + $this->linkBaseYiiFiles(); } } @@ -73,7 +73,7 @@ class Installer extends LibraryInstaller $this->removePackage($package); // remove links for Yii.php if ($package->getName() == 'yiisoft/yii2-dev') { - $this->removeYiiBaseFiles(); + $this->removeBaseYiiFiles(); } } @@ -169,13 +169,13 @@ class Installer extends LibraryInstaller } } - protected function linkYiiBaseFiles() + protected function linkBaseYiiFiles() { $yiiDir = $this->vendorDir . '/yiisoft/yii2/yii'; if (!file_exists($yiiDir)) { mkdir($yiiDir, 0777, true); } - foreach(['Yii.php', 'YiiBase.php', 'classes.php'] as $file) { + foreach(['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { file_put_contents($yiiDir . '/' . $file, <<vendorDir . '/yiisoft/yii2/yii'; - foreach(['Yii.php', 'YiiBase.php', 'classes.php'] as $file) { + foreach(['Yii.php', 'BaseYii.php', 'classes.php'] as $file) { if (file_exists($yiiDir . '/' . $file)) { unlink($yiiDir . '/' . $file); } From 6b95b2ad54d8203192be2279ae602c1223cf8205 Mon Sep 17 00:00:00 2001 From: Pavel Agalecky Date: Mon, 23 Dec 2013 22:11:28 +0400 Subject: [PATCH 69/77] Added support for tagName and encodeLabel parameters in ButtonDropdown --- extensions/yii/bootstrap/ButtonDropdown.php | 14 ++++++++++---- extensions/yii/bootstrap/CHANGELOG.md | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/extensions/yii/bootstrap/ButtonDropdown.php b/extensions/yii/bootstrap/ButtonDropdown.php index 1ffde7d..095a93f 100644 --- a/extensions/yii/bootstrap/ButtonDropdown.php +++ b/extensions/yii/bootstrap/ButtonDropdown.php @@ -49,6 +49,14 @@ class ButtonDropdown extends Widget * @var boolean whether to display a group of split-styled button group. */ public $split = false; + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var boolean whether the label should be HTML-encoded. + */ + public $encodeLabel = true; /** @@ -68,7 +76,6 @@ class ButtonDropdown extends Widget { Html::addCssClass($this->options, 'btn'); if ($this->split) { - $tag = 'button'; $options = $this->options; $this->options['data-toggle'] = 'dropdown'; Html::addCssClass($this->options, 'dropdown-toggle'); @@ -78,7 +85,6 @@ class ButtonDropdown extends Widget 'options' => $this->options, ]); } else { - $tag = 'a'; $this->label .= ' '; $options = $this->options; if (!isset($options['href'])) { @@ -89,10 +95,10 @@ class ButtonDropdown extends Widget $splitButton = ''; } return Button::widget([ - 'tagName' => $tag, + 'tagName' => $this->tagName, 'label' => $this->label, 'options' => $options, - 'encodeLabel' => false, + 'encodeLabel' => $this->encodeLabel, ]) . "\n" . $splitButton; } diff --git a/extensions/yii/bootstrap/CHANGELOG.md b/extensions/yii/bootstrap/CHANGELOG.md index 0eaa722..3cd4aff 100644 --- a/extensions/yii/bootstrap/CHANGELOG.md +++ b/extensions/yii/bootstrap/CHANGELOG.md @@ -7,6 +7,7 @@ Yii Framework 2 bootstrap extension Change Log - Enh #1474: Added option to make NavBar 100% width (cebe) - Enh #1553: Only add navbar-default class to NavBar when no other class is specified (cebe) - Bug #1459: Update Collapse to use bootstrap 3 classes (tonydspaniard) +- Enh: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) 2.0.0 alpha, December 1, 2013 ----------------------------- From 56c361bb9e75b27d5cf1053e3742d6e780ba5820 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 23 Dec 2013 14:55:41 -0500 Subject: [PATCH 70/77] Fixed changelog. --- extensions/yii/bootstrap/CHANGELOG.md | 2 +- framework/CHANGELOG.md | 1 + framework/yii/db/ActiveQuery.php | 5 ----- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/extensions/yii/bootstrap/CHANGELOG.md b/extensions/yii/bootstrap/CHANGELOG.md index 3cd4aff..d6fbee3 100644 --- a/extensions/yii/bootstrap/CHANGELOG.md +++ b/extensions/yii/bootstrap/CHANGELOG.md @@ -6,8 +6,8 @@ Yii Framework 2 bootstrap extension Change Log - Enh #1474: Added option to make NavBar 100% width (cebe) - Enh #1553: Only add navbar-default class to NavBar when no other class is specified (cebe) +- Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Bug #1459: Update Collapse to use bootstrap 3 classes (tonydspaniard) -- Enh: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 41d8f75..dd9d168 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -26,6 +26,7 @@ Yii Framework 2 Change Log - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) +- Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index e0e14cf..e93e9be 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -143,9 +143,4 @@ class ActiveQuery extends Query implements ActiveQueryInterface } return $db->createCommand($sql, $params); } - - public function joinWith($name) - { - - } } From 5fc275e935c7acf4b4b1a05d825d050ffa31877c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 23 Dec 2013 16:36:21 -0500 Subject: [PATCH 71/77] Fixes #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions --- framework/CHANGELOG.md | 1 + framework/yii/grid/ActionColumn.php | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index dd9d168..ba1cff7 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -22,6 +22,7 @@ Yii Framework 2 Change Log - Enh #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue) - Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe) +- Enh #1499: Added `ActionColumn::controller` property to support customizing the controller for handling GridView actions (qiangxue) - Enh #1523: Query conditions now allow to use the NOT operator (cebe) - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) diff --git a/framework/yii/grid/ActionColumn.php b/framework/yii/grid/ActionColumn.php index 707d411..b53b606 100644 --- a/framework/yii/grid/ActionColumn.php +++ b/framework/yii/grid/ActionColumn.php @@ -19,6 +19,13 @@ use yii\helpers\Html; */ class ActionColumn extends Column { + /** + * @var string the ID of the controller that should handle the actions specified here. + * If not set, it will use the currently active controller. This property is mainly used by + * [[urlCreator]] to create URLs for different actions. The value of this property will be prefixed + * to each action name to form the route of the action. + */ + public $controller; public $template = '{view} {update} {delete}'; public $buttons = []; public $urlCreator; @@ -75,7 +82,8 @@ class ActionColumn extends Column return call_user_func($this->urlCreator, $model, $key, $index, $action); } else { $params = is_array($key) ? $key : ['id' => $key]; - return Yii::$app->controller->createUrl($action, $params); + $route = $this->controller ? $this->controller . '/' . $action : $action; + return Yii::$app->controller->createUrl($route, $params); } } From 5e092ac6195aabb4e5f511df433ed29b607dd33a Mon Sep 17 00:00:00 2001 From: Edin Date: Tue, 24 Dec 2013 02:01:54 +0100 Subject: [PATCH 72/77] Fixed sequence id match for postgresql PgAdmin generates sequence as '"Schema"."Table_seq"'::regclass when non public schema is used. --- framework/yii/db/pgsql/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index f6c7298..eb7de37 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/framework/yii/db/pgsql/Schema.php @@ -299,7 +299,7 @@ SQL; $table->columns[$column->name] = $column; if ($column->isPrimaryKey === true) { $table->primaryKey[] = $column->name; - if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { + if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?\.?\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); } } From 2402d2d031724efb5e06c7f61484723c2bc7ee1a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 23 Dec 2013 22:26:44 -0500 Subject: [PATCH 73/77] Draft implementation of ActiveQuery::joinWith(). --- framework/yii/db/ActiveQuery.php | 137 +++++++++++++++++++++++++++ tests/unit/framework/db/ActiveRecordTest.php | 27 ++++++ 2 files changed, 164 insertions(+) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index e93e9be..098724d 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -143,4 +143,141 @@ class ActiveQuery extends Query implements ActiveQueryInterface } return $db->createCommand($sql, $params); } + + public function joinWith($with, $eagerLoading = true, $joinType = 'INNER JOIN') + { + $with = (array)$with; + $this->joinWithRelations(new $this->modelClass, $with, $joinType); + + if (is_array($eagerLoading)) { + foreach ($with as $name => $callback) { + if (is_integer($name)) { + if (!in_array($callback, $eagerLoading, true)) { + unset($with[$name]); + } + } elseif (!in_array($name, $eagerLoading, true)) { + unset($with[$name]); + } + } + $this->with($with); + } elseif ($eagerLoading) { + $this->with($with); + } + return $this; + } + + /** + * @param ActiveRecord $model + * @param array $with + * @param string|array $joinType + */ + private function joinWithRelations($model, $with, $joinType) + { + $relations = []; + + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + + $primaryModel = $model; + $parent = $this; + $prefix = ''; + while (($pos = strpos($name, '.')) !== false) { + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + $fullName = $prefix === '' ? $name : "$prefix.$name"; + if (!isset($relations[$fullName])) { + $relations[$fullName] = $relation = $primaryModel->getRelation($name); + $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); + } else { + $relation = $relations[$fullName]; + } + $primaryModel = new $relation->modelClass; + $parent = $relation; + $prefix = $fullName; + $name = $childName; + } + + $fullName = $prefix === '' ? $name : "$prefix.$name"; + if (!isset($relations[$fullName])) { + $relations[$fullName] = $relation = $primaryModel->getRelation($name); + if ($callback !== null) { + call_user_func($callback, $relation); + } + $this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName)); + } + } + } + + private function getJoinType($joinType, $name) + { + if (is_array($joinType) && isset($joinType[$name])) { + return $joinType[$name]; + } else { + return is_string($joinType) ? $joinType : 'INNER JOIN'; + } + } + + /** + * @param ActiveQuery $query + * @return string + */ + private function getQueryTableName($query) + { + if (empty($query->from)) { + /** @var ActiveRecord $modelClass */ + $modelClass = $query->modelClass; + return $modelClass::tableName(); + } else { + return reset($query->from); + } + } + + /** + * @param ActiveQuery $parent + * @param ActiveRelation $child + * @param string $joinType + */ + private function joinWithRelation($parent, $child, $joinType) + { + $parentTable = $this->getQueryTableName($parent); + $childTable = $this->getQueryTableName($child); + if (!empty($child->link)) { + $on = []; + foreach ($child->link as $childColumn => $parentColumn) { + $on[] = '{{' . $parentTable . "}}.[[$parentColumn]] = {{" . $childTable . "}}.[[$childColumn]]"; + } + $on = implode(' AND ', $on); + } else { + $on = ''; + } + $this->join($joinType, $childTable, $on); + if (!empty($child->where)) { + $this->andWhere($child->where); + } + if (!empty($child->having)) { + $this->andHaving($child->having); + } + if (!empty($child->orderBy)) { + $this->addOrderBy($child->orderBy); + } + if (!empty($child->groupBy)) { + $this->addGroupBy($child->groupBy); + } + if (!empty($child->params)) { + $this->addParams($child->params); + } + if (!empty($child->join)) { + foreach ($child->join as $join) { + $this->join[] = $join; + } + } + if (!empty($child->union)) { + foreach ($child->union as $union) { + $this->union[] = $union; + } + } + } } diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 15462b5..d112ff4 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -217,4 +217,31 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id'])); $this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity'])); } + + public function testJoinWith() + { + // inner join filtering and eager loading + $orders = Order::find()->joinWith([ + 'customer' => function ($query) { + $query->where('tbl_customer.id=2'); + }, + ])->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + + // inner join filtering without eager loading + $orders = Order::find()->joinWith([ + 'customer' => function ($query) { + $query->where('tbl_customer.id=2'); + }, + ], false)->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertFalse($orders[0]->isRelationPopulated('customer')); + $this->assertFalse($orders[1]->isRelationPopulated('customer')); + } } From 4f44bb241697d26816c43baa993a64a2c200f125 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 24 Dec 2013 00:08:49 -0500 Subject: [PATCH 74/77] Fixes #1581: Added `ActiveQuery::joinWith()` to support joining with relations --- docs/guide/active-record.md | 24 ++++++ framework/CHANGELOG.md | 1 + framework/yii/db/ActiveQuery.php | 116 ++++++++++++++++++++++++++- framework/yii/db/ActiveRelationTrait.php | 40 ++++----- tests/unit/data/ar/Category.php | 27 +++++++ tests/unit/data/ar/Item.php | 5 ++ tests/unit/framework/db/ActiveRecordTest.php | 23 ++++++ 7 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 tests/unit/data/ar/Category.php diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 70d41e0..6498dfc 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -386,6 +386,30 @@ $customers = Customer::find()->limit(100)->with([ ``` +Joining with Relations +---------------------- + +When working with relational databases, a common task is to join multiple tables and apply various +query conditions and parameters to the JOIN SQL statement. Instead of calling [[ActiveQuery::join()]] +explicitly to build up the JOIN query, you may reuse the existing relation definitions and call [[ActiveQuery::joinWith()]] +to achieve the same goal. For example, + +```php +// find all orders that contain books, and eager loading "books" +$orders = Order::find()->joinWith('books')->all(); +// find all orders that contain books, and sort the orders by the book names. +$orders = Order::find()->joinWith([ + 'books' => function ($query) { + $query->orderBy('tbl_item.name'); + } +])->all(); +``` + +Note that [[ActiveQuery::joinWith()]] differs from [[ActiveQuery::with()]] in that the former will build up +and execute a JOIN SQL statement. For example, `Order::find()->joinWith('books')->all()` returns all orders that +contain books, while `Order::find()->with('books')->all()` returns all orders regardless they contain books or not. + + Working with Relationships -------------------------- diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ba1cff7..562277e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,6 +27,7 @@ Yii Framework 2 Change Log - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) +- Enh #1581: Added `ActiveQuery::joinWith()` to support joining with relations (qiangxue) - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 098724d..714ff61 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -68,6 +68,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface $rows = $command->queryAll(); if (!empty($rows)) { $models = $this->createModels($rows); + if (!empty($this->join) && $this->indexBy === null) { + $models = $this->removeDuplicatedModels($models); + } if (!empty($this->with)) { $this->findWith($this->with, $models); } @@ -78,6 +81,47 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** + * Removes duplicated models by checking their primary key values. + * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. + * @param array $models the models to be checked + * @return array the distinctive models + */ + private function removeDuplicatedModels($models) + { + $hash = []; + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $pks = $class::primaryKey(); + + if (count($pks) > 1) { + foreach ($models as $i => $model) { + $key = []; + foreach ($pks as $pk) { + $key[] = $model[$pk]; + } + $key = serialize($key); + if (isset($hash[$key])) { + unset($models[$i]); + } else { + $hash[$key] = true; + } + } + } else { + $pk = reset($pks); + foreach ($models as $i => $model) { + $key = $model[$pk]; + if (isset($hash[$key])) { + unset($models[$i]); + } else { + $hash[$key] = true; + } + } + } + + return array_values($models); + } + + /** * Executes query and returns a single row of result. * @param Connection $db the DB connection used to create the DB command. * If null, the DB connection returned by [[modelClass]] will be used. @@ -144,6 +188,42 @@ class ActiveQuery extends Query implements ActiveQueryInterface return $db->createCommand($sql, $params); } + /** + * Joins with the specified relations. + * + * This method allows you to reuse existing relation definitions to perform JOIN queries. + * Based on the definition of the specified relation(s), the method will append one or multiple + * JOIN statements to the current query. + * + * If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations, + * which is equivalent to calling [[with()]] using the specified relations. + * + * Note that because a JOIN query will be performed, you are responsible to disambiguate column names. + * + * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement. + * When `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. + * + * @param array $with the relations to be joined. Each array element represents a single relation. + * The array keys are relation names, and the array values are the corresponding anonymous functions that + * can be used to modify the relation queries on-the-fly. If a relation query does not need modification, + * you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]). + * For example, + * + * ```php + * // find all orders that contain books, and eager loading "books" + * Order::find()->joinWith('books')->all(); + * // find all orders that contain books, and sort the orders by the book names. + * Order::find()->joinWith([ + * 'books' => function ($query) { + * $query->orderBy('tbl_item.name'); + * } + * ])->all(); + * ``` + * + * @param bool $eagerLoading + * @param string $joinType + * @return $this + */ public function joinWith($with, $eagerLoading = true, $joinType = 'INNER JOIN') { $with = (array)$with; @@ -167,9 +247,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** - * @param ActiveRecord $model - * @param array $with - * @param string|array $joinType + * Modifies the current query by adding join fragments based on the given relations. + * @param ActiveRecord $model the primary model + * @param array $with the relations to be joined + * @param string|array $joinType the join type */ private function joinWithRelations($model, $with, $joinType) { @@ -211,6 +292,12 @@ class ActiveQuery extends Query implements ActiveQueryInterface } } + /** + * Returns the join type based on the given join type parameter and the relation name. + * @param string|array $joinType the given join type(s) + * @param string $name relation name + * @return string the real join type + */ private function getJoinType($joinType, $name) { if (is_array($joinType) && isset($joinType[$name])) { @@ -221,8 +308,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** + * Returns the table name used by the specified active query. * @param ActiveQuery $query - * @return string + * @return string the table name */ private function getQueryTableName($query) { @@ -236,14 +324,32 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** + * Joins a parent query with a child query. + * The current query object will be modified accordingly. * @param ActiveQuery $parent * @param ActiveRelation $child * @param string $joinType */ private function joinWithRelation($parent, $child, $joinType) { + $via = $child->via; + $child->via = null; + if ($via instanceof ActiveRelation) { + // via table + $this->joinWithRelation($parent, $via, $joinType); + $this->joinWithRelation($via, $child, $joinType); + return; + } elseif (is_array($via)) { + // via relation + $this->joinWithRelation($parent, $via[1], $joinType); + $this->joinWithRelation($via[1], $child, $joinType); + return; + } + $parentTable = $this->getQueryTableName($parent); $childTable = $this->getQueryTableName($child); + + if (!empty($child->link)) { $on = []; foreach ($child->link as $childColumn => $parentColumn) { @@ -254,6 +360,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface $on = ''; } $this->join($joinType, $childTable, $on); + + if (!empty($child->where)) { $this->andWhere($child->where); } diff --git a/framework/yii/db/ActiveRelationTrait.php b/framework/yii/db/ActiveRelationTrait.php index c885006..dac3028 100644 --- a/framework/yii/db/ActiveRelationTrait.php +++ b/framework/yii/db/ActiveRelationTrait.php @@ -189,26 +189,6 @@ trait ActiveRelationTrait } /** - * @param ActiveRecord|array $model - * @param array $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - if (count($attributes) > 1) { - $key = []; - foreach ($attributes as $attribute) { - $key[] = $model[$attribute]; - } - return serialize($key); - } else { - $attribute = reset($attributes); - $key = $model[$attribute]; - return is_scalar($key) ? $key : serialize($key); - } - } - - /** * @param array $models */ private function filterByModels($models) @@ -237,6 +217,26 @@ trait ActiveRelationTrait } /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = []; + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + return serialize($key); + } else { + $attribute = reset($attributes); + $key = $model[$attribute]; + return is_scalar($key) ? $key : serialize($key); + } + } + + /** * @param array $primaryModels either array of AR instances or arrays * @return array */ diff --git a/tests/unit/data/ar/Category.php b/tests/unit/data/ar/Category.php new file mode 100644 index 0000000..cebacb0 --- /dev/null +++ b/tests/unit/data/ar/Category.php @@ -0,0 +1,27 @@ +hasMany(Item::className(), ['category_id' => 'id']); + } +} diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index e725be9..2d04f9e 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -15,4 +15,9 @@ class Item extends ActiveRecord { return 'tbl_item'; } + + public function getCategory() + { + return $this->hasOne(Category::className(), ['id' => 'category_id']); + } } diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index d112ff4..276547e 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -243,5 +243,28 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(3, $orders[1]->id); $this->assertFalse($orders[0]->isRelationPopulated('customer')); $this->assertFalse($orders[1]->isRelationPopulated('customer')); + + // join with via-relation + $orders = Order::find()->joinWith('books')->orderBy('tbl_order.id')->all(); + $this->assertEquals(2, count($orders)); + $this->assertEquals(1, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertTrue($orders[0]->isRelationPopulated('books')); + $this->assertTrue($orders[1]->isRelationPopulated('books')); + $this->assertEquals(2, count($orders[0]->books)); + $this->assertEquals(1, count($orders[1]->books)); + + // join with sub-relation + $orders = Order::find()->joinWith([ + 'items.category' => function ($q) { + $q->where('tbl_category.id = 2'); + }, + ])->orderBy('tbl_order.id')->all(); + $this->assertEquals(1, count($orders)); + $this->assertTrue($orders[0]->isRelationPopulated('items')); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, count($orders[0]->items)); + $this->assertTrue($orders[0]->items[0]->isRelationPopulated('category')); + $this->assertEquals(2, $orders[0]->items[0]->category->id); } } From b59469b54c8a1cce0c6f2144f10da0969c27dd0a Mon Sep 17 00:00:00 2001 From: zvon Date: Tue, 24 Dec 2013 10:31:16 +0200 Subject: [PATCH 75/77] doc fix --- docs/guide/controller.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/controller.md b/docs/guide/controller.md index 801df69..de6cec5 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -194,9 +194,9 @@ public function behaviors() 'class' => 'yii\web\AccessControl', 'rules' => [ ['allow' => true, 'actions' => ['admin'], 'roles' => ['@']], - ), - ), - ); + ], + ], + ]; } ``` From dc720d9bf4fb8a07f2c7d08012952644ad85e794 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 24 Dec 2013 09:29:05 -0500 Subject: [PATCH 76/77] more docs about joinwith() --- docs/guide/active-record.md | 45 +++++++++++++++++++++++++++++++++++++--- framework/yii/db/ActiveQuery.php | 10 ++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 6498dfc..0996331 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -400,14 +400,53 @@ $orders = Order::find()->joinWith('books')->all(); // find all orders that contain books, and sort the orders by the book names. $orders = Order::find()->joinWith([ 'books' => function ($query) { - $query->orderBy('tbl_item.name'); + $query->orderBy('tbl_item.id'); } ])->all(); ``` Note that [[ActiveQuery::joinWith()]] differs from [[ActiveQuery::with()]] in that the former will build up -and execute a JOIN SQL statement. For example, `Order::find()->joinWith('books')->all()` returns all orders that -contain books, while `Order::find()->with('books')->all()` returns all orders regardless they contain books or not. +and execute a JOIN SQL statement for the primary model class. For example, `Order::find()->joinWith('books')->all()` +returns all orders that contain books, while `Order::find()->with('books')->all()` returns all orders +regardless they contain books or not. + +Because `joinWith()` will cause generating a JOIN SQL statement, you are responsible to disambiguate column +names. For example, we use `tbl_item.id` to disambiguate the `id` column reference because both of the order table +and the item table contain a column named `id`. + +You may join with one or multiple relations. You may also join with sub-relations. For example, + +```php +// join with multiple relations +// find out the orders that contain books and are placed by customers who registered within the past 24 hours +$orders = Order::find()->joinWith([ + 'books', + 'customer' => function ($query) { + $query->where('tbl_customer.create_time > ' . (time() - 24 * 3600)); + } +])->all(); +// join with sub-relations: join with books and books' authors +$orders = Order::find()->joinWith('books.author')->all(); +``` + +By default, when you join with a relation, the relation will also be eagerly loaded. You may change this behavior +by passing the `$eagerLoading` parameter which specifies whether to eager load the specified relations. + +Also, when the relations are joined with the primary table, the default join type is `INNER JOIN`. You may change +to use other type of joins, such as `LEFT JOIN`. + +Below are some more examples, + +```php +// find all orders that contain books, but do not eager loading "books". +$orders = Order::find()->joinWith('books', false)->all(); +// find all orders and sort them by the customer IDs. Do not eager loading "customer". +$orders = Order::find()->joinWith([ + 'customer' => function ($query) { + $query->orderBy('tbl_customer.id'); + }, +], false, 'LEFT JOIN')->all(); +``` Working with Relationships diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 714ff61..a71b1cf 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -220,9 +220,13 @@ class ActiveQuery extends Query implements ActiveQueryInterface * ])->all(); * ``` * - * @param bool $eagerLoading - * @param string $joinType - * @return $this + * @param boolean|array $eagerLoading whether to eager load the relations specified in `$with`. + * When this is a boolean, it applies to all relations specified in `$with`. Use an array + * to explicitly list which relations in `$with` need to be eagerly loaded. + * @param string|array $joinType the join type of the relations specified in `$with`. + * When this is a string, it applies to all relations specified in `$with`. Use an array + * in the format of `relationName => joinType` to specify different join types for different relations. + * @return static the query object itself */ public function joinWith($with, $eagerLoading = true, $joinType = 'INNER JOIN') { From 03451912455dcf43229b92f8afa463448ec43fa0 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 24 Dec 2013 21:27:13 -0500 Subject: [PATCH 77/77] Added ActiveQuery::innerJoinWith(). --- docs/guide/active-record.md | 61 +++++++++++++++------------- framework/CHANGELOG.md | 2 +- framework/yii/db/ActiveQuery.php | 24 ++++++++--- tests/unit/framework/db/ActiveRecordTest.php | 18 ++++++-- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 0996331..a414bce 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -391,35 +391,26 @@ Joining with Relations When working with relational databases, a common task is to join multiple tables and apply various query conditions and parameters to the JOIN SQL statement. Instead of calling [[ActiveQuery::join()]] -explicitly to build up the JOIN query, you may reuse the existing relation definitions and call [[ActiveQuery::joinWith()]] -to achieve the same goal. For example, +explicitly to build up the JOIN query, you may reuse the existing relation definitions and call +[[ActiveQuery::joinWith()]] to achieve this goal. For example, ```php +// find all orders and sort the orders by the customer id and the order id. also eager loading "customer" +$orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id, tbl_order.id')->all(); // find all orders that contain books, and eager loading "books" -$orders = Order::find()->joinWith('books')->all(); -// find all orders that contain books, and sort the orders by the book names. -$orders = Order::find()->joinWith([ - 'books' => function ($query) { - $query->orderBy('tbl_item.id'); - } -])->all(); +$orders = Order::find()->innerJoinWith('books')->all(); ``` -Note that [[ActiveQuery::joinWith()]] differs from [[ActiveQuery::with()]] in that the former will build up -and execute a JOIN SQL statement for the primary model class. For example, `Order::find()->joinWith('books')->all()` -returns all orders that contain books, while `Order::find()->with('books')->all()` returns all orders -regardless they contain books or not. - -Because `joinWith()` will cause generating a JOIN SQL statement, you are responsible to disambiguate column -names. For example, we use `tbl_item.id` to disambiguate the `id` column reference because both of the order table -and the item table contain a column named `id`. +In the above, the method [[ActiveQuery::innerJoinWith()|innerJoinWith()]] is a shortcut to [[ActiveQuery::joinWith()|joinWith()]] +with the join type set as `INNER JOIN`. -You may join with one or multiple relations. You may also join with sub-relations. For example, +You may join with one or multiple relations; you may apply query conditions to the relations on-the-fly; +and you may also join with sub-relations. For example, ```php // join with multiple relations // find out the orders that contain books and are placed by customers who registered within the past 24 hours -$orders = Order::find()->joinWith([ +$orders = Order::find()->innerJoinWith([ 'books', 'customer' => function ($query) { $query->where('tbl_customer.create_time > ' . (time() - 24 * 3600)); @@ -429,23 +420,37 @@ $orders = Order::find()->joinWith([ $orders = Order::find()->joinWith('books.author')->all(); ``` +Behind the scene, Yii will first execute a JOIN SQL statement to bring back the primary models +satisfying the conditions applied to the JOIN SQL. It will then execute a query for each relation +and populate the corresponding related records. + +The difference between [[ActiveQuery::joinWith()|joinWith()]] and [[ActiveQuery::with()|with()]] is that +the former joins the tables for the primary model class and the related model classes to retrieve +the primary models, while the latter just queries against the table for the primary model class to +retrieve the primary models. + +Because of this difference, you may apply query conditions that are only available to a JOIN SQL statement. +For example, you may filter the primary models by the conditions on the related models, like the example +above. You may also sort the primary models using columns from the related tables. + +When using [[ActiveQuery::joinWith()|joinWith()]], you are responsible to disambiguate column names. +In the above examples, we use `tbl_item.id` and `tbl_order.id` to disambiguate the `id` column references +because both of the order table and the item table contain a column named `id`. + By default, when you join with a relation, the relation will also be eagerly loaded. You may change this behavior by passing the `$eagerLoading` parameter which specifies whether to eager load the specified relations. -Also, when the relations are joined with the primary table, the default join type is `INNER JOIN`. You may change -to use other type of joins, such as `LEFT JOIN`. +And also by default, [[ActiveQuery::joinWith()|joinWith()]] uses `LEFT JOIN` to join the related tables. +You may pass it with the `$joinType` parameter to customize the join type. As a shortcut to the `INNER JOIN` type, +you may use [[ActiveQuery::innerJoinWith()|innerJoinWith()]]. Below are some more examples, ```php // find all orders that contain books, but do not eager loading "books". -$orders = Order::find()->joinWith('books', false)->all(); -// find all orders and sort them by the customer IDs. Do not eager loading "customer". -$orders = Order::find()->joinWith([ - 'customer' => function ($query) { - $query->orderBy('tbl_customer.id'); - }, -], false, 'LEFT JOIN')->all(); +$orders = Order::find()->innerJoinWith('books', false)->all(); +// equivalent to the above +$orders = Order::find()->joinWith('books', false, 'INNER JOIN')->all(); ``` diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 562277e..d94011f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -27,7 +27,7 @@ Yii Framework 2 Change Log - Enh #1552: It is now possible to use multiple bootstrap NavBar in a single page (Alex-Code) - Enh #1572: Added `yii\web\Controller::createAbsoluteUrl()` (samdark) - Enh #1579: throw exception when the given AR relation name does not match in a case sensitive manner (qiangxue) -- Enh #1581: Added `ActiveQuery::joinWith()` to support joining with relations (qiangxue) +- Enh #1581: Added `ActiveQuery::joinWith()` and `ActiveQuery::innerJoinWith()` to support joining with relations (qiangxue) - Enh #1601: Added support for tagName and encodeLabel parameters in ButtonDropdown (omnilight) - Enh: Added `favicon.ico` and `robots.txt` to defauly application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index a71b1cf..26b0c6e 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -200,8 +200,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface * * Note that because a JOIN query will be performed, you are responsible to disambiguate column names. * - * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement. - * When `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. + * This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement + * for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations. * * @param array $with the relations to be joined. Each array element represents a single relation. * The array keys are relation names, and the array values are the corresponding anonymous functions that @@ -211,8 +211,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface * * ```php * // find all orders that contain books, and eager loading "books" - * Order::find()->joinWith('books')->all(); - * // find all orders that contain books, and sort the orders by the book names. + * Order::find()->joinWith('books', true, 'INNER JOIN')->all(); + * // find all orders, eager loading "books", and sort the orders and books by the book names. * Order::find()->joinWith([ * 'books' => function ($query) { * $query->orderBy('tbl_item.name'); @@ -228,7 +228,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface * in the format of `relationName => joinType` to specify different join types for different relations. * @return static the query object itself */ - public function joinWith($with, $eagerLoading = true, $joinType = 'INNER JOIN') + public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') { $with = (array)$with; $this->joinWithRelations(new $this->modelClass, $with, $joinType); @@ -251,6 +251,20 @@ class ActiveQuery extends Query implements ActiveQueryInterface } /** + * Inner joins with the specified relations. + * This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN". + * Please refer to [[joinWith()]] for detailed usage of this method. + * @param array $with the relations to be joined with + * @param boolean|array $eagerLoading whether to eager loading the relations + * @return static the query object itself + * @see joinWith() + */ + public function innerJoinWith($with, $eagerLoading = true) + { + return $this->joinWith($with, $eagerLoading, 'INNER JOIN'); + } + + /** * Modifies the current query by adding join fragments based on the given relations. * @param ActiveRecord $model the primary model * @param array $with the relations to be joined diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 276547e..40050e5 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -220,8 +220,18 @@ class ActiveRecordTest extends DatabaseTestCase public function testJoinWith() { + // left join and eager loading + $orders = Order::find()->joinWith('customer')->orderBy('tbl_customer.id DESC, tbl_order.id')->all(); + $this->assertEquals(3, count($orders)); + $this->assertEquals(2, $orders[0]->id); + $this->assertEquals(3, $orders[1]->id); + $this->assertEquals(1, $orders[2]->id); + $this->assertTrue($orders[0]->isRelationPopulated('customer')); + $this->assertTrue($orders[1]->isRelationPopulated('customer')); + $this->assertTrue($orders[2]->isRelationPopulated('customer')); + // inner join filtering and eager loading - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'customer' => function ($query) { $query->where('tbl_customer.id=2'); }, @@ -233,7 +243,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue($orders[1]->isRelationPopulated('customer')); // inner join filtering without eager loading - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'customer' => function ($query) { $query->where('tbl_customer.id=2'); }, @@ -245,7 +255,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertFalse($orders[1]->isRelationPopulated('customer')); // join with via-relation - $orders = Order::find()->joinWith('books')->orderBy('tbl_order.id')->all(); + $orders = Order::find()->innerJoinWith('books')->orderBy('tbl_order.id')->all(); $this->assertEquals(2, count($orders)); $this->assertEquals(1, $orders[0]->id); $this->assertEquals(3, $orders[1]->id); @@ -255,7 +265,7 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(1, count($orders[1]->books)); // join with sub-relation - $orders = Order::find()->joinWith([ + $orders = Order::find()->innerJoinWith([ 'items.category' => function ($q) { $q->where('tbl_category.id = 2'); },